mpris.el/mpris.el

1690 lines
67 KiB
EmacsLisp

;;; mpris.el --- Client to the Media Player Remote Interface Specification -*- lexical-binding: t; -*-
;;
;; Copyright (C) 2024 TEC
;;
;; Author: TEC <contact@tecosaur.net>
;; Maintainer: TEC <contact@tecosaur.net>
;; Created: March 05, 2024
;; Modified: March 05, 2024
;; Version: 0.1.0
;; Keywords: convenience multimedia tools
;; Homepage: https://code.tecosaur.net/tec/mpris.el
;; Package-Requires: ((emacs "28.1"))
;;
;; This file is not part of GNU Emacs.
;;
;;; Commentary:
;;
;; Client to the Media Player Remote Interface Specification.
;;
;; Query and control media players over DBus.
;;
;;; Code:
(require 'dbus)
(require 'url)
(defgroup mpris nil
"Client via the Media Player Remote Interface Specification (MPRIS)."
:group 'multimedia
:prefix "mpris-")
(defcustom mpris-preferred-players nil
"List of preferred media players, in priority order.
Each player should be represended by a string that matches the MPRIS service
name \"org.mpris.MediaPlayer2.Player.*\". Most commonly, this is simply the
application name."
:type '(repeat string))
(defcustom mpris-disliked-players
'("kdeconnect" "firefox" "chromium" "plasma-browser-integration")
"List of disliked media players, only selected when there are no other options.
Each player should be represended by a string that matches the MPRIS service
name \"org.mpris.MediaPlayer2.Player.*\". Most commonly, this is simply the
application name."
:type '(repeat string))
(defcustom mpris-bus-address :session
"The DBus address to use.
Should be `:session', `:system', or a string denoting the bus address."
:type '(choice
(const :session)
(const :system)
string))
(defcustom mpris-current-player-change-hook nil
"Hooks to run when the current player changes.
Each hook function is called with two arguments: the previous player service and
the new player service."
:type 'hook)
(defcustom mpris-playback-status-change-hook nil
"Hooks to run when track metadata changes.
Each hook function is called with two arguments: the service whose playback
status has changed, and the new playback status."
:type 'hook)
(defcustom mpris-metadata-change-hook nil
"Hooks to run when track metadata changes.
Each hook function is called with two arguments: the service whose track
metadata has changed, and the new metadata."
:type 'hook)
(defcustom mpris-current-status-hook nil
"Hooks to run when anything about the current playback changes.
Each hook function is called with two arguments, the first giving the nature
of the change, the second giving more information on it. This takes one of three
forms:
1. (playback NEW-PLAYBACK-STATUS)
2. (metadata NEW-METADATA)
3. (player NEW-CURRENT-PLAYER-SERVICE)"
:type 'hook)
(defconst mpris--dbus-path "/org/mpris/MediaPlayer2")
(defconst mpris--dbus-interface "org.mpris.MediaPlayer2")
(defconst mpris--dbus-interface-player "org.mpris.MediaPlayer2.Player")
(defconst mpris--dbus-interface-tracklist "org.mpris.MediaPlayer2.TrackList")
(defconst mpris--dbus-interface-playlists "org.mpris.MediaPlayer2.Playlists")
(defvar mpris-current-player nil
"The primary DBus service communicated with.")
(defvar mpris--active-players nil
"List of DBus services for MPRIS-compatible players.")
(defvar mpris--recent-players nil
"List of players recently selected as the current player.")
(defvar mpris--player-states nil
"List of information for `mpris--active-players'.
Entries are controlled by `mpris--create-player-watcher' and
`mpris--remove-player-watcher'.
Each entry is of the form:
\\=(SERVICE-NAME
:watcher DBUS-WATCHER
:playback-status STATUS-STRING
:metadata METADATA-LIST)")
(defvar mpris--is-setup nil
"Indicator of whether MPRIS is setup up.
Practically, this referres to wheter all the relevant watchers are set up.")
(defvar mpris--dbus-player-existance-watcher nil
"DBus watcher that looks for new and deleted services.")
(defvar mpris--after-sync-callbacks nil
"List of funcalls queued to be executed on the next watcher syncronisation.")
(defvar mpris--player-interfaces nil)
;;; Syncronous internal API
(defun mpris--call-method-sync (service interface method args)
"Call METHOD of the current player's INTERFACE, with ARGS.
To use a different player, set SERVICE to the target MediaPlayer2 service."
(apply #'dbus-call-method
mpris-bus-address (or service mpris-current-player)
mpris--dbus-path interface
method args))
(defun mpris--get-property-sync (service interface property)
"Get PROPERTY of the current player's INTERFACE.
To use a different player, set SERVICE to the target MediaPlayer2 service."
(dbus-get-property
mpris-bus-address (or service mpris-current-player)
mpris--dbus-path interface
property))
(defun mpris--set-property-sync (service interface property value)
"Set PROPERTY of the current player's INTERFACE to VALUE.
To use a different player, set SERVICE to the target MediaPlayer2 service."
(dbus-set-property
mpris-bus-address (or service mpris-current-player)
mpris--dbus-path interface
property value))
;;; Async internal API
(defun mpris--call-method-async (handler service interface method args)
"Asyncronously call METHOD of the current player's INTERFACE, with ARGS.
HANDLER is called on the result of the method call.
To use a different player, set SERVICE to the target MediaPlayer2 service."
(apply #'dbus-call-method-asynchronously
mpris-bus-address (or service mpris-current-player)
mpris--dbus-path interface
method handler args))
(defun mpris--get-property-async (handler service interface property)
"Asyncronously get PROPERTY of the current player's INTERFACE.
HANDLER is called on the result of the method call.
To use a different player, set SERVICE to the target MediaPlayer2 service."
(dbus-call-method-asynchronously
mpris-bus-address (or service mpris-current-player)
mpris--dbus-path dbus-interface-properties
"Get" handler
:timeout 500 interface property))
(defun mpris--set-property-async (handler service interface property value)
"Asyncronously set PROPERTY of the current player's INTERFACE to VALUE.
HANDLER is then called on the result.
To use a different player, set SERVICE to the target MediaPlayer2 service."
(dbus-call-method-asynchronously
mpris-bus-address (or service mpris-current-player)
mpris--dbus-path dbus-interface-properties
"Set" handler
:timeout 500 interface property
(cons :variant (list value))))
;;; Combined Sync/Async internal API
(defun mpris--call-method (async-handler service interface method &rest args)
"Call METHOD of the current player's INTERFACE, with ARGS.
If ASYNC-HANDLER is non-nil, this call will be made asyncronously
and ASYNC-HANDLER called on the result.
To use a different player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--checked-call
#'mpris--call-method-sync #'mpris--call-method-async
async-handler service
interface method args))
(defun mpris--get-property (async-handler service interface property)
"Get PROPERTY of the current player's INTERFACE.
If ASYNC-HANDLER is non-nil, this call will be made asyncronously
and ASYNC-HANDLER called on the result.
To use a different player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--checked-call
#'mpris--get-property-sync #'mpris--get-property-async
async-handler service interface property))
(defun mpris--set-property (async-handler service interface property value)
"Set PROPERTY of the current player's INTERFACE to VALUE.
If ASYNC-HANDLER is non-nil, this call will be made asyncronously
and ASYNC-HANDLER called on the result.
To use a different player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--checked-call
#'mpris--set-property-sync #'mpris--set-property-async
async-handler service interface property value))
(defun mpris--checked-call (sync-fn async-fn async-handler service &rest args)
"Perform a syncronous or asyncronous call, checking the mpris state.
When ASYNC-FN is provided, it will be called like so:
\\=(apply ASYNC-FN ASYNC-HANDLER SERVICE ARGS)
Otherwise SYNC-FN will be called like so:
\\=(apply SYNC-FN SERVICE ARGS)"
(cond
((and mpris--is-setup (not service) (not mpris-current-player))
'no-player)
((and mpris--is-setup async-handler)
(apply async-fn async-handler service args))
(async-handler
(push (append (list async-fn async-handler service) args)
mpris--after-sync-callbacks)
(mpris--setup-async))
(mpris--is-setup
(apply sync-fn service args))
(service
(mpris--setup-async)
(apply sync-fn service args))
(t
(mpris--setup-sync)
(if mpris-current-player
(apply sync-fn service args)
'no-player))))
;;; State updates
(defun mpris--setup-async ()
"Register signal handlers for the current players."
(mpris--teardown)
(mpris--list-dbus-services
(mpris--update-service-list
#'mpris--setup-1)))
(defun mpris--setup-1 ()
"Fulfil the promise of `mpris--setup-async'."
(unless mpris--is-setup
(setq mpris--is-setup t)
(unless mpris--dbus-player-existance-watcher
(setq mpris--dbus-player-existance-watcher
(dbus-register-signal
:session
dbus-interface-dbus
dbus-path-dbus
dbus-interface-dbus
"NameOwnerChanged"
#'mpris--handle-dbus-name-owner-changed)))
(mapc #'mpris--create-player-watcher mpris--active-players)
(unless mpris--active-players
(mapc (lambda (fn-args) (apply (car fn-args) (cdr fn-args)))
mpris--after-sync-callbacks)
(setq mpris--after-sync-callbacks nil))))
(defun mpris--setup-sync ()
"Perform `mpris--setup-async', but syncronously."
(mpris--update-service-list
(dbus-call-method
mpris-bus-address dbus-service-dbus dbus-path-dbus
dbus-interface-dbus "ListNames"))
(mpris--setup-1))
(defun mpris--teardown ()
"Unregister any watchers created by `mpris--setup-async'."
(when mpris--dbus-player-existance-watcher
(dbus-unregister-object mpris--dbus-player-existance-watcher)
(setq mpris--dbus-player-existance-watcher nil))
(mapc #'mpris--remove-player-watcher mpris--player-states)
(setq mpris--is-setup nil))
(defun mpris--create-player-watcher (player-service)
"Create a watcher for PLAYER-SERVICE and add it to `mpris--player-states'.
Also ensure PLAYER-SERVICE has an entry in `mpris--player-interfaces'."
(mpris--inspect-interfaces player-service)
(let ((watcher
(dbus-register-signal
:session
player-service
mpris--dbus-path
dbus-interface-properties
"PropertiesChanged"
(mpris--create-player-property-change-handler player-service)
mpris--dbus-interface-player)))
(push (list player-service :watcher watcher :playback-status nil :metadata nil)
mpris--player-states)
(mpris--update-playback-status player-service)
(mpris--update-metadata player-service)))
(defun mpris--remove-player-watcher (player-service)
"Cleanly remove PLAYER-SERVICE from `mpris--player-states'."
(when-let ((state (if (consp player-service) player-service
(assoc player-service mpris--player-states))))
(when (plist-get (cdr state) :watcher)
(if (member player-service mpris--active-players)
(dbus-unregister-object (plist-get (cdr state) :watcher))
;; When the service itself has been removed, DBus has already removed
;; signal watchers, and so we only need to update Emacs' table to
;; reflect the new situation.
(remhash (plist-get (cdr state) :watcher) dbus-registered-objects-table)))
(setq mpris--player-states (delq state mpris--player-states))))
(defun mpris--create-player-property-change-handler (service)
"Create a property change handler for SERVICE.
The handler accepts a \"PropertiesChanged\" DBus event, and
modifies `mpris--player-states' appropriately to reflect the new information."
(lambda (_interface changes _)
(when-let ((state (cdr (assoc service mpris--player-states))))
(dolist (change changes)
(let ((change-type (car-safe change))
(value (caadr change)))
(cond
((not (stringp change-type)))
((and (equal change-type "PlaybackStatus")
(stringp value))
(plist-put state :playback-status value)
(run-hook-with-args 'mpris-playback-status-change-hook service value)
(when (equal service mpris-current-player)
(run-hook-with-args 'mpris-current-status-hook 'playback value))
(mpris-update-current-player))
((and (equal change-type "Metadata")
(consp value))
(plist-put state :metadata value)
(run-hook-with-args 'mpris-metadata-change-hook service value)
(when (equal service mpris-current-player)
(run-hook-with-args 'mpris-current-status-hook 'metadata value)))))))))
(defun mpris--handle-dbus-name-owner-changed (name old-owner new-owner)
"Handle an DBus ownership change where NAME has gone from OLD-OWNER to NEW-OWNER."
(when (string-prefix-p "org.mpris.MediaPlayer2." name)
(cond
((string-empty-p old-owner) ; Created service
(push name mpris--active-players)
(mpris--create-player-watcher name))
((string-empty-p new-owner) ; Deleted service
(setq mpris--active-players (delete name mpris--active-players))
(mpris--remove-player-watcher name)
(when (equal name mpris-current-player)
(mpris-update-current-player))))))
(defun mpris--update-playback-status (service-name)
"Update the playback-status entry for SERVICE-NAME in `mpris--player-states'."
(mpris--get-property-async
(lambda (status)
(mpris--update-state-attr service-name :playback-status (car-safe status)))
service-name
mpris--dbus-interface-player
"PlaybackStatus"))
(defun mpris--update-metadata (service-name)
"Update the metadata entry for SERVICE-NAME in `mpris--player-states'."
(mpris--get-property-async
(lambda (metadata)
(mpris--update-state-attr service-name :metadata (car-safe metadata)))
service-name
mpris--dbus-interface-player
"Metadata"))
(defun mpris--update-state-attr (service-name attr value)
"Set SERVICE-NAME's recorded ATTR to VALUE in `mpris--player-states'."
(let (incomplete did-initialise-attr)
(dolist (state mpris--player-states)
(when (equal (car state) service-name)
(unless (plist-get (cdr state) attr)
(setq did-initialise-attr t))
(plist-put (cdr state) attr value))
(unless incomplete
(when (memq nil (cdr state))
(setq incomplete t))))
;; If `did-initialise-attr' and not `incomplete', this means we've just
;; finished synchronising `mpris--player-states' (likely after adding
;; a new player). As such, it makes sense to update the current player
;; and run the after-sync callbacks.
(when (and did-initialise-attr (not incomplete))
(mpris-update-current-player)
(mapc (lambda (fn-args) (apply (car fn-args) (cdr fn-args)))
mpris--after-sync-callbacks)
(setq mpris--after-sync-callbacks nil))))
(defun mpris--list-dbus-services (callback)
"Fetch a list of DBus services, then call CALLBACK on it."
(dbus-call-method-asynchronously
mpris-bus-address dbus-service-dbus dbus-path-dbus
dbus-interface-dbus "ListNames" callback))
(defun mpris--update-service-list (service-list-or-callback &rest args)
"Update the service list, possible running a callback in the process.
SERVICE-LIST-OR-CALLBACK can either be:
1. A list of services, as given by `mpris--list-dbus-services'.
This function then filters them to MPRIS services and updates
`mpris--active-players'.
2. A callback function, in which case a closure will be generated that
when invoked performs (1) and then also invokes the callback with ARGS."
(cond
((functionp service-list-or-callback)
(lambda (service-list)
(mpris--update-service-list service-list)
(apply service-list-or-callback args)))
((consp service-list-or-callback)
(setq mpris--active-players nil)
(dolist (service service-list-or-callback)
(when (string-prefix-p "org.mpris.MediaPlayer2." service)
(push service mpris--active-players))))))
(defun mpris--inspect-interfaces (service)
"Populate `mpris--player-interfaces' with information on SERVICE's interfaces."
(unless (assoc service mpris--player-interfaces)
(let (data)
(dolist (interface (dbus-introspect-get-interface-names
mpris-bus-address service mpris--dbus-path))
(let ((methods (dbus-introspect-get-method-names
mpris-bus-address service mpris--dbus-path interface))
(properties (dbus-introspect-get-property-names
mpris-bus-address service mpris--dbus-path interface)))
(push (list interface methods properties) data)))
(push (cons service data) mpris--player-interfaces))))
(defun mpris--player-interface-p (service interface)
"Return non-nil if SERVICE implements INTERFACE."
(and (assoc interface (cdr (assoc service mpris--player-interfaces))) t))
(defun mpris--player-method-p (service interface method)
"Return non-nil if SERVICE implements INTERFACE and METHOD."
(and (member method (cadr (assoc interface (cdr (assoc service mpris--player-interfaces))))) t))
(defun mpris--player-property-p (service interface property)
"Return non-nil if SERVICE implements INTERFACE and has PROPERTY."
(and (member property (caddr (assoc interface (cdr (assoc service mpris--player-interfaces))))) t))
;;; The current player
(defun mpris-update-current-player ()
"Check and update the player currently being interfaced with.
This relies on active DBus watchers maintaining `mpris--active-players'
and `mpris--player-states'."
;; If the current player exists and is playing, do nothing.
(unless (and (member mpris-current-player mpris--active-players)
(equal "Playing" (mpris-get-playback-status))
(let (disliked-player-p)
(dolist (player mpris-disliked-players)
(when (string-prefix-p (concat "org.mpris.MediaPlayer2." player) mpris-current-player)
(setq disliked-player-p t)))
(not disliked-player-p)))
(let ((candidate-services mpris--active-players)
(previous-service mpris-current-player)
(service-sorter (lambda (a b) (> (cdr a) (cdr b))))
preferred-services recent-services common-services disliked-services
selected-service)
;; Process the candidate services into seperate priority lists.
(dolist (service candidate-services)
(let (name)
(dotimes (o (- (length service) 23))
(when (and (not name) (= (aref service (+ 23 o)) ?.))
(setq name (substring service 23 (+ 23 o)))))
(unless name
(setq name (substring service 23)))
(cond
((member service mpris--recent-players)
(push (cons service (length (member service mpris--recent-players)))
recent-services))
((member name mpris-disliked-players)
(push (cons service (- (length (member name mpris-disliked-players))))
disliked-services))
((member name mpris-preferred-players)
(push (cons service (length (member name mpris-preferred-players)))
preferred-services))
(t (push service common-services)))))
;; This next bit might look weird, but we actually want the ordering of
;; `mpris-disliked-players' and `mpris-preferred-players' to matter, and
(setq recent-services (mapcar #'car (sort recent-services service-sorter)))
(setq preferred-services (mapcar #'car (sort preferred-services service-sorter)))
(setq disliked-services (mapcar #'car (sort disliked-services service-sorter)))
;; First, look for playing services. With priority order:
;; 1. Preferred
;; 2. Recently played
;; 3. Remaining (not disliked)
;; 4. Disliked services
;; Then try again with paused services.
(dolist (playback-state '("Playing" "Paused"))
(dolist (service-sublist
(list recent-services preferred-services common-services disliked-services))
(dolist (service service-sublist)
(unless selected-service
(when (equal playback-state (mpris-get-playback-status nil service))
(setq selected-service service))))))
;; If no service has been selected so far, apply the same priority order
;; but now just looking for any service that exists.
(unless selected-service
(setq selected-service
(or (car recent-services)
(car preferred-services)
(car common-services)
(car disliked-services))))
;; Now do what we promised and set the current player.
(setq mpris-current-player selected-service)
;; If we've selected a different player to the current one, a few things need updating.
(unless (equal previous-service selected-service)
(when (equal "Playing" (mpris-get-playback-status nil selected-service))
(setq mpris--recent-players (delete selected-service mpris--recent-players))
(push selected-service mpris--recent-players))
(run-hook-with-args 'mpris-current-player-change-hook
previous-service selected-service)
(run-hook-with-args 'mpris-current-status-hook 'player selected-service)))))
(defun mpris--check-current-player (&optional callback-fn &rest callback-args)
"Return non-nil if a player currently exists.
If no player exists, return nil and looks for a player in the background.
Optionally, CALLBACK-FN and CALLBACK-ARGS can be provided. The
callback is either invoked immediately if a current player
exists, or after the background player update completes."
(cond
((and mpris-current-player callback-fn)
(apply callback-fn callback-args))
(mpris-current-player t)
((and callback-fn (not mpris--dbus-player-existance-watcher))
(push (cons callback-fn callback-args) mpris--after-sync-callbacks)
(mpris--setup-async))))
;;; MPRIS API
;; Here we define all the method calls and property acessors
;; listed in <https://specifications.freedesktop.org/mpris-spec/2.2/Media_Player.html>
;;;; MPRIS API - Methods
(defun mpris-raise (&optional async-handler service)
"Bring the current player's user interface to the front, if possible.
The media player may be unable to control how its user interface
is displayed, or it may not have a graphical user interface at
all. In this case, the `mpris-can-raise' property is false and
this method does nothing.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--call-method async-handler service mpris--dbus-interface "Raise"))
(defun mpris-quit (&optional async-handler service)
"Stop running the current player.
The media player may refuse to allow clients to shut it down. In
this case, the `mpris-can-quit' property is false and this method
does nothing.
Note: Media players which can be D-Bus activated, or for which
there is no sensibly easy way to terminate a running
instance (via the main interface or a notification area icon for
example) should allow clients to use this method. Otherwise, it
should not be needed.
If the media player does not have a UI, this should be implemented.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--call-method async-handler service mpris--dbus-interface "Quit"))
;;;; MPRIS API - Properties
(defun mpris-can-quit (&optional async-handler service)
"Whether quit is expected to have an effect.
If false, calling `mpris-quit' will have no effect, and may raise
a NotSupported error. If true, calling `mpris-quit' will cause
the media application to attempt to quit (although it may still
be prevented from quitting by the user, for example).
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--get-property async-handler service
mpris--dbus-interface "CanQuit"))
(defun mpris-get-fullscreen (&optional async-handler service)
"Whether the current player is occupying the fullscreen.
This is typically used for videos. A value of true indicates that
the media player is taking up the full screen.
Media centre software may well have this value fixed to true
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(if (mpris--player-property-p (or service mpris-current-player) mpris--dbus-interface "Fullscreen")
(mpris--get-property async-handler service mpris--dbus-interface "Fullscreen")
'unimplemented))
(defun mpris-set-fullscreen (value &optional async-handler service)
"Set the current player fullscreen status to VALUE (boolean).
If `mpris-can-set-fullscreen' is true, clients may set this
property to true to tell the media player to enter fullscreen
mode, or to false to return to windowed mode.
If `mpris-can-set-fullscreen' is false, then attempting to set
this property should have no effect, and may raise an error.
However, even if it is true, the media player may still be unable
to fulfil the request, in which case attempting to set this
property will have no effect (but should not raise an error).
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(if (mpris--player-property-p (or service mpris-current-player) mpris--dbus-interface "Fullscreen")
(mpris--set-property async-handler service mpris--dbus-interface
"Fullscreen" value)
'unimplemented))
(defun mpris-can-set-fullscreen (&optional async-handler service)
"Whether the current player's fullscreen state can be controlled.
If false, attempting `mpris-set-fullscreen' will have no effect,
and may raise an error. If true, attempting
`mpris-set-fullscreen' will not raise an error, and (if it is
different from the current value) will cause the media player to
attempt to enter or exit fullscreen mode.
Note that the media player may be unable to fulfil the request.
In this case, the value will not change. If the media player
knows in advance that it will not be able to fulfil the request,
however, this property should be false.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(if (mpris--player-property-p
(or service mpris-current-player) mpris--dbus-interface "CanSetFullscreen")
(mpris--get-property async-handler service mpris--dbus-interface "CanSetFullscreen")
'unimplemented))
(defun mpris-can-raise (&optional async-handler service)
"Whether the current player can be raised.
If false, calling `mpris-raise' will have no effect, and may
raise a NotSupported error. If true, calling `mpris-raise' will
cause the media application to attempt to bring its user
interface to the front, although it may be prevented from doing
so (by the window manager, for example).
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--get-property async-handler service
mpris--dbus-interface "CanRaise"))
(defun mpris-has-track-list (&optional async-handler service)
"Indicate whether the current player implements the TrackList interface.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--get-property async-handler service
mpris--dbus-interface "HasTrackList"))
(defun mpris-identity (&optional async-handler service)
"A friendly name to identify the current player to users.
This should usually match the name found in .desktop files.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--get-property async-handler service
mpris--dbus-interface "Identity"))
(defun mpris-desktop-entry (&optional async-handler service)
"The desktop entry of the current player.
The basename of an installed .desktop file which complies with
the Desktop entry specification, with the \".desktop\" extension stripped.
Example: The desktop entry file is
\"/usr/share/applications/vlc.desktop\", and this property
contains \"vlc\".
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--get-property async-handler service
mpris--dbus-interface "DesktopEntry"))
(defun mpris-supported-uri-schemes (&optional async-handler service)
"The URI schemes supported by the current player.
This can be viewed as protocols supported by the player in almost
all cases. Almost every media player will include support for the
\"file\" scheme. Other common schemes are \"http\" and \"rtsp\".
Note that URI schemes should be lower-case.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--get-property async-handler service
mpris--dbus-interface "SupportedUriSchemes"))
(defun mpris-supported-mime-types (&optional async-handler service)
"The mime-types supported by the current player.
Mime-types should be in the standard format (eg: \"audio/mpeg\"
or \"application/ogg\").
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--get-property async-handler service
mpris--dbus-interface "SupportedMimeTypes"))
;;; MPRIS Player API
;; Here we define all the method calls and property acessors
;; listed in <https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html>
;;;; MPRIS Player API - Methods
(defun mpris-next (&optional async-handler service)
"Skip to the next track in the tracklist.
If there is no next track (and endless playback and track repeat are both off),
stop playback. If playback is paused or stopped, it remains that way.
If `mpris-can-go-next' is false, attempting to call this method
should have no effect.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called on completion with a single nil argument.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(interactive (list #'ignore))
(mpris--call-method async-handler service mpris--dbus-interface-player "Next"))
(defun mpris-previous (&optional async-handler service)
"Skips to the previous track in the tracklist.
If there is no previous track (and endless playback and track repeat are both
off), stop playback. If playback is paused or stopped, it remains that way.
If `mpris-can-go-previous' is false, attempting to call this
method should have no effect.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called on completion with a single nil argument.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(interactive (list #'ignore))
(mpris--call-method async-handler service mpris--dbus-interface-player "Previous"))
(defun mpris-pause (&optional async-handler service)
"Pauses playback.
If playback is already paused, this has no effect. Calling Play after this
should cause playback to start again from the same position.
If `mpris-can-pause' is false, attempting to call this method
should have no effect.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called on completion with a single nil argument.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(interactive (list #'ignore))
(mpris--call-method async-handler service mpris--dbus-interface-player "Pause"))
(defun mpris-play-pause (&optional async-handler service)
"Pause or resume playback.
If playback is already paused, resumes playback. If playback is stopped, starts
playback.
If `mpris-can-pause' is false, attempting to call this method
should have no effect and raise an error.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called on completion with a single nil argument.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(interactive (list #'ignore))
(mpris--call-method async-handler service mpris--dbus-interface-player "PlayPause"))
(defun mpris-stop (&optional async-handler service)
"Stops playback.
If playback is already stopped, this has no effect. Calling Play after this
should cause playback to start again from the beginning of the track.
If `mpris-can-control' is false, attempting to call this method
should have no effect and raise an error.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called on completion with a single nil argument.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(interactive (list #'ignore))
(mpris--call-method async-handler service mpris--dbus-interface-player "Stop"))
(defun mpris-play (&optional async-handler service)
"Start or resume playback.
If already playing, this has no effect.
If paused, playback resumes from the current position.
If there is no track to play, this has no effect.
If `mpris-can-play' is false, attempting to call this method
should have no effect.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called on completion with a single nil argument.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(interactive (list #'ignore))
(mpris--call-method async-handler service mpris--dbus-interface-player "Play"))
(defun mpris-seek (offset &optional async-handler service)
"Seeks forward in the current track by OFFSET microseconds.
A negative value seeks back. If this would mean seeking back further than the
start of the track, the position is set to 0. If the value passed in would mean
seeking beyond the end of the track, acts like a call to Next.
If the `mpris-can-seek' property is false, this has no effect.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called on completion with a single nil argument.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--call-method async-handler service mpris--dbus-interface-player
"Seek" :int64 offset))
(defun mpris-set-position (track-id position &optional async-handler service)
"Set the current track (TRACK-ID) playback to POSITION microseconds.
If POSITION argument is less than 0, do nothing.
If POSITION argument is greater than the track length, do nothing.
If the `mpris-can-seek' property is false, this has no effect.
The TRACK-ID argument is required to avoid race conditions where
a client tries to seek a position after the track has already
been changed.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called on completion with a single nil argument.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--call-method async-handler service mpris--dbus-interface-player
"SetPosition" :object-path track-id :int64 position))
(defun mpris-open-uri (uri &optional async-handler service)
"Open URI with the current player.
The uri scheme should be an element of
`mpris-supported-uri-schemes' and the mime-type should match one
of the elements of the `mpris-supported-mime-types'.
If the uri scheme or the mime-type of the uri to open is not
supported, this method does nothing and may raise an error. In
particular, if the list of available uri schemes is empty, this
method may not be implemented.
Clients should not assume that URI has been opened as soon as
this method returns. They should wait until the mpris:trackid
field in the Metadata property changes.
If the media player implements the TrackList interface, then the
opened track should be made part of the tracklist, the
org.mpris.MediaPlayer2.TrackList.TrackAdded or
org.mpris.MediaPlayer2.TrackList.TrackListReplaced signal should
be fired, as well as the
org.freedesktop.DBus.Properties.PropertiesChanged signal on the
tracklist interface.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called on completion with a single nil argument.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--call-method async-handler service mpris--dbus-interface-player
"OpenUri" uri))
;;; MPRIS Player API - Properties
(defun mpris-get-playback-status (&optional async-handler service)
"Get the current playback status.
Either \"Playing\", \"Paused\", or \"Stopped\".
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(if-let ((value
(plist-get
(cdr (assoc (or service mpris-current-player) mpris--player-states))
:playback-status)))
(if async-handler
(funcall async-handler value)
value)
(mpris--get-property async-handler service
mpris--dbus-interface-player "PlaybackStatus")))
(defun mpris-get-loop-status (&optional async-handler service)
"Get the current loop/repeat status.
May be:
- \"None\" if the playback will stop when there are no more tracks to play.
- \"Track\" if the current track will start again from the begining once it has
finished playing.
- \"Playlist\" if the playback loops through a list of tracks.
This is an *optional* part of the MPRIS2 specification, if the current player
does not support this option the symbol \\='unimplemented will be returned.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(if (mpris--player-property-p
(or service mpris-current-player) mpris--dbus-interface-player "LoopStatus")
(mpris--get-property async-handler service mpris--dbus-interface-player
"LoopStatus")
'unimplemented))
(defun mpris-set-loop-status (value &optional async-handler service)
"Set the current loop/repeat status to VALUE.
VALUE may be:
- \"None\" the playback will stop when there are no more tracks to play.
- \"Track\" the current track will start again from the begining once it has
finished playing.
- \"Playlist\" the playback loops through a list of tracks.
This is an *optional* part of the MPRIS2 specification, if the current player
does not support this option the symbol \\='unimplemented will be returned.
If `mpris-can-control' is false, attempting to set this property
should have no effect and raise an error.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(if (mpris--player-property-p
(or service mpris-current-player) mpris--dbus-interface-player "LoopStatus")
(mpris--set-property async-handler service mpris--dbus-interface-player
"LoopStatus" value)
'unimplemented))
(defun mpris-get-rate (&optional async-handler service)
"Get the current playback rate.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--get-property async-handler service
mpris--dbus-interface-player "Rate"))
(defun mpris-set-rate (value &optional async-handler service)
"Set the current playback rate to VALUE.
The value must fall in the range described by `mpris-get-minimum-rate' and
MaximumRate, and must not be 0.0. If playback is paused, the
PlaybackStatus property should be used to indicate this. A value
of 0.0 should not be set by the client. If it is, the media
player should act as though `mpris-pause' was called.
If the media player has no ability to play at speeds other than
the normal playback rate, this must still be implemented, and
must return 1.0. The `mpris-get-minimum-rate' and
`mpris-get-maximum-rate' properties must also be set to 1.0.
Not all values may be accepted by the media player. It is left to
media player implementations to decide how to deal with values
they cannot use; they may either ignore them or pick a best fit
value. Clients are recommended to only use sensible fractions or
multiples of 1 (eg: 0.5, 0.25, 1.5, 2.0, etc).
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--set-property async-handler service mpris--dbus-interface-player
"Rate" value))
(defun mpris-get-shuffle (&optional async-handler service)
"Whether the playlist is progressing in a non-linear order.
This is an *optional* part of the MPRIS2 specification, if the current player
does not support this option the symbol \\='unimplemented will be returned.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(if (mpris--player-property-p
(or service mpris-current-player) mpris--dbus-interface-player "Shuffle")
(mpris--get-property async-handler service mpris--dbus-interface-player
"Shuffle")
'unimplemented))
(defun mpris-set-shuffle (value &optional async-handler service)
"Set the playlist to progress in a non-linear order depending on VALUE.
VALUE should be either t or nil.
This is an *optional* part of the MPRIS2 specification, if the current player
does not support this option the symbol \\='unimplemented will be returned.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(if (mpris--player-property-p
(or service mpris-current-player) mpris--dbus-interface-player "Shuffle")
(mpris--set-property async-handler service mpris--dbus-interface-player
"Shuffle" value)
'unimplemented))
(defun mpris-get-metadata (&optional async-handler service)
"Get the metadata of the current track.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(if-let ((value
(plist-get
(cdr (assoc (or service mpris-current-player) mpris--player-states))
:metadata)))
(if async-handler
(funcall async-handler value)
value)
(mpris--get-property async-handler service
mpris--dbus-interface-player "Metadata")))
(defun mpris-get-volume (&optional async-handler service)
"Get the volume level.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--get-property async-handler service
mpris--dbus-interface-player "Volume"))
(defun mpris-set-volume (value &optional async-handler service)
"Set the volume level to VALUE.
When setting, if a negative value is passed, the volume should be
set to 0.0.
If `mpris-can-control' is false, attempting to set this property
should have no effect and raise an error.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--set-property async-handler service mpris--dbus-interface-player
"Volume" value))
(defun mpris-get-position (&optional async-handler service)
"The current track position in microseconds.
The position is between 0 and the \"mpris:length\" metadata
entry (see Metadata).
Note: If the media player allows it, the current playback
position can be changed with either `mpris-set-position' or
`mpris-seek'. If this is not the case, the `mpris-can-seek'
property is false, and setting this property has no effect and
can raise an error.
If the playback progresses in a way that is inconstistant with
the Rate property (`mpris-get-rate'), the Seeked signal is
emited.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--get-property async-handler service
mpris--dbus-interface-player "Position"))
(defun mpris-get-minimum-rate (&optional async-handler service)
"The minimum value which the Rate property can take.
Clients should not call `mpris-set-rate' with a lower value.
Note that even if this value is 0.0 or negative, `mpris-set-rate'
should not be called with a value of 0.0 or below.
This value should always be 1.0 or less.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--get-property async-handler service
mpris--dbus-interface-player "MinimumRate"))
(defun mpris-get-maximum-rate (&optional async-handler service)
"The maximum value which the Rate property can take.
Clients should not call `mpris-set-rate' with a greater value.
This value should always be 1.0 or greater.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--get-property async-handler service
mpris--dbus-interface-player "MaximumRate"))
(defun mpris-can-go-next (&optional async-handler service)
"Whether `mpris-next' is expected to change the current track.
More specifically, whether the client can call `mpris-next' and
expect the current track to change.
If it is unknown whether a `mpris-next' call will be
successful (for example, when streaming tracks), this property
should be set to true.
If `mpris-can-control' is false, this property should also be
false.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--get-property async-handler service
mpris--dbus-interface-player "CanGoNext"))
(defun mpris-can-go-previous (&optional async-handler service)
"Whether `mpris-previous' is expected to change the current track.
More specifically, whether the client can call `mpris-previous'
and expect the current track to change.
If it is unknown whether a `mpris-previous' call will be
successful (for example, when streaming tracks), this property
should be set to true.
If `mpris-can-control' is false, this property should also be false.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--get-property async-handler service
mpris--dbus-interface-player "CanGoPrevious"))
(defun mpris-can-play (&optional async-handler service)
"Whether playback can be started using `mpris-play' or `mpris-play-pause'.
Note that this is related to whether there is a \"current
track\": the value should not depend on whether the track is
currently paused or playing. In fact, if a track is currently
playing (and `mpris-can-control' is true), this should be true.
If `mpris-can-control' is false, this property should also be
false.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--get-property async-handler service
mpris--dbus-interface-player "CanPlay"))
(defun mpris-can-pause (&optional async-handler service)
"Whether playback can be paused using `mpris-pause' or `mpris-play-pause'.
Note that this is an intrinsic property of the current track: its
value should not depend on whether the track is currently paused
or playing. In fact, if playback is currently paused (and
CanControl is true), this should be true.
If `mpris-can-control' is false, this property should also be
false.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--get-property async-handler (or service mpris-current-player)
mpris--dbus-interface-player "CanPause"))
(defun mpris-can-seek (&optional async-handler service)
"Whether `mpris-seek' and `mpris-set-position' can control the playback position.
This may be different for different tracks.
If `mpris-can-control' is false, this property should also be
false.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--get-property async-handler service
mpris--dbus-interface-player "CanSeek"))
(defun mpris-can-control (&optional async-handler service)
"Whether the media player may be controlled over this interface.
This property is not expected to change, as it describes an
intrinsic capability of the implementation.
If this is false, clients should assume that all properties on
this interface are read-only (and will raise errors if writing to
them is attempted), no methods are implemented and all other
properties starting with \"Can\" are also false.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris--get-property async-handler service
mpris--dbus-interface-player "CanControl"))
(defun mpris-open-file (file &optional async-handler service)
"Open FILE with the current player.
A thin wrapper around `mpris-open-uri'.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called on completion with a single nil argument.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(mpris-open-uri
(url-encode-url (mpris--file-name-uri file))
async-handler service))
;;; MPRIS TrackList API
;; Here we define all the method calls and property acessors
;; listed in <https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html>
;;;; MPRIS TrackList API - Methods
(defun mpris-get-tracks-metadata (track-ids &optional async-handler service)
"Gets all the metadata available for a set of tracks.
TRACK-IDS is the list of track ids for which metadata is requested.
Each set of metadata must have a \"mpris:trackid\" entry at the
very least, which contains a string that uniquely identifies this
track within the scope of the tracklist.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(if (mpris--player-interface-p (or service mpris-current-player) mpris--dbus-interface-tracklist)
(mpris--call-method async-handler service mpris--dbus-interface-tracklist
"GetTracksMetadata" '(:array :object-path) track-ids)
'unimplemented))
(defun mpris-add-track (uri after-track-id set-as-current &optional async-handler service)
"Add a URI to the TrackList.
- URI is the uri of the item to add. Its uri scheme should be an
element of the `mpris-supported-uri-schemes' property and the
mime-type should match one of the elements of
`mpris-supported-mime-types'.
- AFTER-TRACK-ID is the identifier of the track after which the
new item should be inserted. The path
\"/org/mpris/MediaPlayer2/TrackList/NoTrack\" indicates that
the track should be inserted at the start of the track list.
- SET-AS-CURRENT indicates whether the newly inserted track
should be considered as the current track. Setting this to true
has the same effect as calling GoTo afterwards.
If `mpris-can-edit-tracks' is false, this has no effect.
Note: Clients should not assume that the track has been added at
the time when this method returns. They should wait for a
TrackAdded (or TrackListReplaced) signal.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(if (mpris--player-interface-p (or service mpris-current-player) mpris--dbus-interface-tracklist)
(mpris--call-method async-handler service mpris--dbus-interface-tracklist
"AddTrack" :string uri :object-path after-track-id :boolean set-as-current)
'unimplemented))
(defun mpris-remove-track (track-id &optional async-handler service)
"Remove an item from the TrackList.
- TRACK-ID is the identifier of the track to be removed.
\"/org/mpris/MediaPlayer2/TrackList/NoTrack\" is not a valid
value for this argument.
If the track is not part of this tracklist, this has no effect.
If the `mpris-can-edit-tracks' property is false, this has no effect.
Note: Clients should not assume that the track has been removed
at the time when this method returns. They should wait for a
TrackRemoved (or TrackListReplaced) signal.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(if (mpris--player-interface-p (or service mpris-current-player) mpris--dbus-interface-tracklist)
(mpris--call-method async-handler service mpris--dbus-interface-tracklist
"RemoveTrack" :object-path track-id)
'unimplemented))
(defun mpris-go-to-track (track-id &optional async-handler service)
"Skip to the specified TRACK-ID.
- TRACK-ID is the identifier of the track to skip to.
\"/org/mpris/MediaPlayer2/TrackList/NoTrack\" is not a valid
value for this argument.
If the track is not part of this tracklist, this has no effect.
If this object is not \"/org/mpris/MediaPlayer2\", the current
TrackList's tracks should be replaced with the contents of this
TrackList, and the TrackListReplaced signal should be fired from
\"/org/mpris/MediaPlayer2\".
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(if (mpris--player-interface-p (or service mpris-current-player) mpris--dbus-interface-tracklist)
(mpris--call-method async-handler service mpris--dbus-interface-tracklist
"GoTo" :object-path track-id)
'unimplemented))
;;;; MPRIS TrackList API - Properties
(defun mpris-get-tracks (&optional async-handler service)
"Get each track in the current tracklist, in order.
The org.freedesktop.DBus.Properties.PropertiesChanged signal is
emited every time this property changes, but the signal message
does not contain the new value. Client implementations should
rather rely on the TrackAdded, TrackRemoved and TrackListReplaced
signals to keep their representation of the tracklist up to date.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(if (mpris--player-interface-p (or service mpris-current-player) mpris--dbus-interface-tracklist)
(mpris--get-property async-handler service mpris--dbus-interface-tracklist "Tracks")
'unimplemented))
(defun mpris-can-edit-tracks (&optional async-handler service)
"Whether the track list can be edited.
If false, calling `mpris-add-track' or `mpris-remove-track' will
have no effect, and may raise a NotSupported error.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(if (mpris--player-interface-p (or service mpris-current-player) mpris--dbus-interface-tracklist)
(mpris--get-property async-handler service mpris--dbus-interface-tracklist "")
'unimplemented))
;;; MPRIS Playlists API
;; Here we define all the method calls and property acessors
;; listed in <https://specifications.freedesktop.org/mpris-spec/latest/Playlists_Interface.html>
;;;; MPRIS Playlists API - Methods
(defun mpris-activate-playlist (playlist-id &optional async-handler service)
"Start playing PLAYLIST-ID.
It is up to the media player whether this completely replaces the
current tracklist, or whether it is merely inserted into the
tracklist and the first track starts. For example, if the media
player is operating in a \"jukebox\" mode, it may just append the
playlist to the list of upcoming tracks, and skip to the first
track in the playlist.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(if (mpris--player-interface-p (or service mpris-current-player) mpris--dbus-interface-playlists)
(mpris--call-method async-handler service mpris--dbus-interface-playlists
"ActivatePlaylist" :object-path playlist-id)
'unimplemented))
(defun mpris-get-playlists (index max-count order reverse-order &optional async-handler service)
"Get a set of playlists.
- INDEX is the integer index of the first playlist to be
fetched (according to the ordering).
- MAX-COUNT is the maximum number of playlists to fetch.
- ORDER specifies the ordering that should be used, it should be one of
`mpris-get-orderings', usually:
- \"Alphabetical\"
- \"CreationDate\"
- \"ModifiedDate\"
- \"LastPlayDate\"
- \"UserDefined\"
- REVERSE-ORDER specifies whether the order should be reversed.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(if (mpris--player-interface-p (or service mpris-current-player) mpris--dbus-interface-playlists)
(mpris--call-method async-handler service mpris--dbus-interface-playlists
"GetPlaylists" :int64 index :int64 max-count order :boolean reverse-order)
'unimplemented))
;;;; MPRIS Playlists API - Properties
(defun mpris-get-playlist-count (&optional async-handler service)
"The number of playlists available.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(if (mpris--player-interface-p (or service mpris-current-player) mpris--dbus-interface-playlists)
(mpris--get-property async-handler service mpris--dbus-interface-playlists
"PlaylistCount")
'unimplemented))
(defun mpris-get-orderings (&optional async-handler service)
"The availible orderings.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(if (mpris--player-interface-p (or service mpris-current-player) mpris--dbus-interface-playlists)
(mpris--get-property async-handler service mpris--dbus-interface-playlists
"Orderings")
'unimplemented))
(defun mpris-active-playlist (&optional async-handler service)
"The currently-active playlist.
If there is no currently-active playlist, the structure's Valid
field will be false, and the Playlist details are undefined.
Note that this may not have a value even after ActivatePlaylist
is called with a valid playlist id as ActivatePlaylist
implementations have the option of simply inserting the contents
of the playlist into the current tracklist.
When ASYNC-HANDLER is set, the call is made asynchronously and the function
called with the result.
To use a specific player, set SERVICE to the target MediaPlayer2 service.
If SERVICE is unset and no player exists, the symbol no-player is returned."
(if (mpris--player-interface-p (or service mpris-current-player) mpris--dbus-interface-playlists)
(mpris--get-property async-handler service mpris--dbus-interface-playlists
"ActivePlaylist")
'unimplemented))
;;; MPRIS Utility functions
(defun mpris-track-attr (attr &optional service)
"Attempt to determine ATTR of the current track.
ATTR should either be a string correspanding to a metadata entry, or a symbol
from the following list:
- trackid
- title
- album
- artist (or artists)
- album-artist
- length
- genre
- url or file
- rating
- art-url or art-file
To use a specific player, set SERVICE to the target MediaPlayer2 service."
(let ((key
(pcase attr
('trackid "mpris:trackid")
('title "xesam:title")
('album "xesam:album")
((or 'artist 'artists) "xesam:artist")
('album-artist "xesam:albumArtist")
('length "mpris:length")
('genre "xesam:genre")
((or 'url 'file) "xesam:url")
('rating "xesam:userRating")
((or 'art-url 'art-file)
"mpris:artUrl")
((pred stringp) attr)
(_ (error "Unrecognised attribute: %s" attr))))
(value-extractor
(pcase attr
((or 'artist 'album-artist) #'caar)
((or 'file 'art-file)
(lambda (v)
(and (car v)
(mpris--uri-to-file (car v)))))
(_ #'car)))
(metadata (mpris-get-metadata nil service)))
(and (not (eq metadata 'no-player))
(funcall value-extractor (cadr (assoc key metadata))))))
(defun mpris--uri-to-file (uri)
"Try to get a file path for the content referred to by URI."
(if (string-prefix-p "file://" uri)
(url-filename (url-generic-parse-url uri))
(let ((temp-file
(file-name-concat
temporary-file-directory
(file-name-with-extension
(concat "emacs-mpris-" (sha1 uri))
(or (file-name-extension uri) "img")))))
(if (file-exists-p temp-file)
temp-file
(and (url-copy-file uri temp-file)
temp-file)))))
(defun mpris--file-name-uri (f)
"Return a URI for the filename F.
Copy of `rng-file-name-uri'."
(setq f (expand-file-name f))
(let ((uri
(replace-regexp-in-string "[]\0-\s\"#%;<>?[\\^`{|}\177]"
#'mpris--percent-encode f)))
(concat "file:"
(if (and (> (length uri) 0)
(= (aref uri 0) ?/))
"//"
"///")
uri)))
(defun mpris--percent-encode (str)
"Percent encode every char in STR.
Used in `mpris--file-name-uri'"
(apply #'concat
(mapcar (lambda (ch)
(format "%%%x%x" (/ ch 16) (% ch 16)))
(string-to-list str))))
(provide 'mpris)
;;; mpris.el ends here