branch: externals/listen
commit 51043cc9442ab2068d621b3d5b6151e3c686d50f
Author: Adam Porter <[email protected]>
Commit: Adam Porter <[email protected]>
WIP
This mostly works, but there are still things to fix. For example:
- When the player is paused, the duration in the lighter still counts
down. Will have to store the position in a slot and update that, and
calculate remaining from that; and stop updating the position when the
player is paused; and when the player is unpaused, set the started-at
and started-from accordingly. Ugh. This is why I tried to avoid
caching this information and just fetch it from the player every time.
---
listen-lib.el | 8 ++-
listen-mpv.el | 223 +++++++++++++++++++++++++++++++++++++++-------------------
listen.el | 27 ++++---
3 files changed, 169 insertions(+), 89 deletions(-)
diff --git a/listen-lib.el b/listen-lib.el
index 0af5d79edb..e3f9f04f80 100644
--- a/listen-lib.el
+++ b/listen-lib.el
@@ -106,7 +106,13 @@ keywords are supported:
process command args
(max-volume
100 :documentation "Maximum volume in percent (may be greater than 100 for
some players).")
- etc)
+ etc status
+ (path nil :documentation "Filename path or URL to currently playing track,
if any.")
+ volume
+ playback-started-at playback-started-from
+ ;; (position nil :documentation "Position in current track, in seconds.")
+ (duration nil :documentation "Duration of current track, in seconds.")
+ (metadata nil :documentation "Metadata alist."))
(cl-defstruct listen-queue
name tracks current etc)
diff --git a/listen-mpv.el b/listen-mpv.el
index 8702897761..7a3f63f970 100755
--- a/listen-mpv.el
+++ b/listen-mpv.el
@@ -52,15 +52,26 @@
;;;; Functions
+(cl-defmethod listen--got-property ((player listen-player-mpv) msg)
+ (pcase-let (((map event id reason data error name) msg))
+ (pcase data
+ ("metadata"))))
+
(cl-defmethod listen--info ((player listen-player-mpv))
"Return metadata from MPV PLAYER, or nil if a track is not playing."
- ;; If the metadata property isn't available, ignore the error.
- (when-let ((metadata (ignore-errors (listen-mpv--get-property player
"metadata"))))
- (map-apply (lambda (key value)
- ;; TODO: Consider using symbols as keys (VLC returns strings,
MPV's decodes as
- ;; symbols).
- (cons (downcase (symbol-name key)) value))
- metadata)))
+ (or (listen-player-metadata player)
+ (listen--update-metadata player)))
+
+(cl-defmethod listen--update-metadata ((player listen-player-mpv) &optional
(callback #'ignore))
+ (let ((callback
+ (lambda (msg)
+ (pcase-let (((map event id reason data error name) msg))
+ (setf (listen-player-metadata player)
+ (map-apply (lambda (key value)
+ (cons (intern (downcase (symbol-name key)))
value))
+ data))
+ (funcall callback)))))
+ (listen-mpv--update-property player callback "metadata")))
(cl-defmethod listen--filename ((player listen-player-mpv))
"Return filename of PLAYER's current track."
@@ -69,14 +80,16 @@
(match-string 1 status))))
(cl-defmethod listen--title ((player listen-player-mpv))
- (listen-mpv--get-property player "media-title" ))
+ (map-elt (listen-player-metadata player) 'title))
(cl-defmethod listen--ensure ((player listen-player-mpv))
"Ensure PLAYER is ready."
(pcase-let* (((cl-struct listen-player command args process) player)
(socket (make-temp-name (expand-file-name "listen-mpv-socket-"
temporary-file-directory)))
(args (append args (list (format "--input-ipc-server=%s" socket)
- (format "--volume=%s"
listen-mpv-volume)))))
+ "--msg-level=ipc=debug"
+ (format "--volume=%s"
listen-mpv-volume)
+ "--terminal=no"))))
(unless (process-live-p process)
(let ((process-buffer (generate-new-buffer " *listen-player-mpv*"))
(socket-buffer (generate-new-buffer "
*listen-player-mpv-socket*")))
@@ -90,8 +103,84 @@
(setf (map-elt (listen-player-etc player) :network-process)
(make-network-process :name "listen-player-mpv-socket" :family
'local
:remote socket :noquery t
- :buffer socket-buffer)))
- (set-process-query-on-exit-flag (listen-player-process player) nil))))
+ :buffer socket-buffer)
+ (process-filter (map-elt (listen-player-etc player)
:network-process))
+ (lambda (proc text)
+ (listen--filter player proc text))
+ (process-sentinel (map-elt (listen-player-etc player)
:network-process))
+ (lambda (proc msg)
+ (display-warning 'listen-mpv
+ (format-message "listen-process-sentinel:
PROC:%S MSG:%S"
+ proc msg)
+ :debug "*listen-mpv*")
+ (internal-default-process-sentinel proc msg))))
+ (set-process-query-on-exit-flag (listen-player-process player) nil)
+ ;; Observe relevant properties.
+ (dolist (property '("volume" "mute" "pause" "playback-time" "duration"
"path" "metadata"))
+ (listen--send player "observe_property" property)))))
+
+(cl-defmethod listen--filter ((player listen-player-mpv) proc text)
+ (listen-debug :buffer "*listen-mpv*" (listen-player-process player) proc
text)
+ (cl-labels ((next-message ()
+ (if-let ((msg (ignore-errors (json-read))))
+ (progn
+ (listen-debug :buffer "*listen-mpv*" "Parsed" msg)
+ (delete-region (point-min) (point))
+ msg)
+ ;; Unparseable: return point so we can try again later.
+ (listen-debug :buffer "*listen-mpv*" "Unparseable")
+ (goto-char (point-min))
+ nil)))
+ (with-current-buffer (process-buffer proc)
+ (goto-char (point-max))
+ (insert text)
+ (goto-char (point-min))
+ (while-let ((msg (next-message)))
+ (listen--act player msg)))))
+
+(cl-defmethod listen--act ((player listen-player-mpv) msg)
+ (listen-debug :buffer "*listen-mpv*" (listen-player-process player) msg)
+ (pcase-let (((map event request_id reason data error name) msg))
+ (pcase event
+ ((or "start-file" "playback-restart")
+ (listen--status-is player 'playing)
+ (setf (listen-player-playback-started-at player) (current-time)
+ (listen-player-playback-started-from player) 0)
+ (listen--update-metadata player)
+ (listen-mpv--update-property
+ player (lambda (msg)
+ (setf (listen-player-duration player)
+ (map-elt msg 'data)))
+ "duration")
+ (listen-mpv--update-property
+ player (lambda (msg)
+ (setf (listen-player-volume player)
+ (map-elt msg 'data)))
+ "volume"))
+ ((or "end-file" "idle") (listen--status-is player 'stopped))
+ ((or 'nil "data")
+ (if-let ((callback (map-elt (map-elt (listen-player-etc player)
:requests) request_id)))
+ (prog1
+ (funcall callback msg)
+ (setf (map-elt (listen-player-etc player) :requests)
+ (map-delete (map-elt (listen-player-etc player) :requests)
request_id)))
+ (listen-debug :buffer "*listen-mpv*" "No callback for" msg)))
+ ("property-change"
+ (pcase name
+ ("duration" (setf (listen-player-duration player) data))
+ ("metadata" (setf (listen-player-metadata player) data))
+ ("path" (setf (listen-player-path player) data))
+ ("pause" (listen--status-is
+ player (pcase data
+ ('t 'paused)
+ ('nil 'playing)
+ (_ (listen-debug :buffer "*listen-mpv*"
"Unrecognized pause" data)))))
+ ;; ("playback-time" (setf (listen-player-position player) data))
+ ("volume" (setf (listen-player-volume player) data))))
+ (_ (listen-debug :buffer "*listen-mpv*" "Unrecognized event" event)))))
+
+(cl-defmethod listen--status-is ((player listen-player-mpv) status)
+ (setf (listen-player-status player) status))
(cl-defmethod listen--play ((player listen-player-mpv) file)
"Play FILE with PLAYER.
@@ -103,81 +192,58 @@ Stops playing, clears playlist, adds FILE, and plays it."
;; (listen--send player "stop"))
(cl-defmethod listen--status ((player listen-player-mpv))
- (if (and (listen--playing-p player)
- (not (listen-mpv--get-property listen-player "pause")))
- "playing"
- ;; TODO: Consider using "eof-reached" proeprty.
- (if (listen-mpv--get-property listen-player "pause")
- "paused"
- "stopped")))
+ (or (listen-player-status player)
+ (setf (listen-player-status player)
+ (if (and (listen--playing-p player)
+ (not (listen-mpv--get-property listen-player "pause")))
+ 'playing
+ ;; TODO: Consider using "eof-reached" proeprty.
+ (if (listen-mpv--get-property listen-player "pause")
+ 'paused
+ 'stopped)))))
(cl-defmethod listen--pause ((player listen-player-mpv))
"Pause playing with PLAYER."
- (if (listen-mpv--get-property player "pause")
- (listen-mpv--set-property player "pause" "no")
- (listen-mpv--set-property player "pause" "yes")))
+ (let* ((status (pcase (listen-player-status player)
+ ('playing "yes")
+ ('paused "no")
+ ('nil "no")))
+ (request-id (listen-mpv--set-property player "pause" status)))
+ (setf (map-elt (map-elt (listen-player-etc player) :requests) request-id)
+ (lambda (msg)
+ (pcase (map-elt msg 'error)
+ ("success" (setf (listen-player-status player)
+ (pcase status
+ ("yes" 'paused)
+ ("no" 'playing)))))))))
(cl-defmethod listen--playing-p ((player listen-player-mpv))
"Return non-nil if PLAYER is playing."
- (not (listen-mpv--get-property player "idle-active")))
+ (equal (listen-player-status player) 'playing))
(cl-defmethod listen--elapsed ((player listen-player-mpv))
"Return seconds elapsed for PLAYER's track."
- (listen-mpv--get-property player "time-pos"))
+ (+ (time-to-seconds
+ (time-subtract (current-time) (listen-player-playback-started-at
player)))
+ (listen-player-playback-started-from player)))
(cl-defmethod listen--length ((player listen-player-mpv))
"Return length of PLAYER's track in seconds."
- (listen-mpv--get-property player "duration"))
+ (listen-player-duration player))
(require 'json)
(cl-defmethod listen--send ((player listen-player-mpv) command &rest args)
- "Send COMMAND to PLAYER and return output."
+ "Send COMMAND to PLAYER and return request ID."
(listen--ensure player)
(pcase-let* (((cl-struct listen-player (etc (map :network-process))) player)
(request-id (cl-incf (map-elt (listen-player-etc player)
:request-id))))
- (with-current-buffer (process-buffer network-process)
- (let ((json (json-encode `(("command" ,command ,@args)
- ("request_id" . ,request-id)))))
- ;; (message "SENDING: %S" json)
- (process-send-string network-process json)
- (process-send-string network-process "\n")
- (goto-char (point-max))
- (with-local-quit
- (accept-process-output network-process 2))
- (save-excursion
- (goto-char (point-min))
- (let ((json-false nil))
- (cl-loop
- ;; do (message "BUFFER-CONTENTS:%S POS:%s BUFFER-SIZE:%s
EOBP:%s"
- ;; (buffer-string) (point) (buffer-size) (eobp))
- until (or (eobp) (looking-at-p (rx (0+ space) eos)))
- for start-pos = (point)
- for result = (condition-case-unless-debug err
- (json-read)
- (error
- (message "listen--send: JSON-READ signaled error:
%S BUFFER-CONTENTS:%S POS:%s BUFFER-SIZE:%s EOBP:%s"
- err (buffer-string) (point)
(buffer-size) (eobp))))
- while result
- for value = (pcase (map-elt result 'request_id)
- ((pred (equal request-id))
- ;; Event is the one we're looking for: delete the
event from the
- ;; buffer and return it.
- (unless listen-debug-p
- (delete-region start-pos (point)))
- result)
- ('nil
- ;; Event has no request ID: delete it from the
buffer.
- (unless listen-debug-p
- (delete-region start-pos (point)))
- nil)
- (_
- ;; Event is for a different request: ignore it
(this probably
- ;; won't happen in practice, since we process
commands
- ;; synchronously, but it's good to be careful).
- nil))
- when value
- return value)))))))
+ (let ((json (json-encode `(("command" ,command ,@args)
+ ("request_id" . ,request-id)))))
+ (listen-debug :buffer "*listen-mpv*" (listen-player-process player) json)
+ (process-send-string network-process json)
+ (process-send-string network-process "\n"))
+ request-id))
(cl-defmethod listen--seek ((player listen-player-mpv) seconds)
"Seek PLAYER to SECONDS."
@@ -186,13 +252,25 @@ Stops playing, clears playlist, adds FILE, and plays it."
(cl-defmethod listen--volume ((player listen-player-mpv) &optional volume)
"Return or set PLAYER's VOLUME.
VOLUME is an integer percentage."
- (pcase-let (((cl-struct listen-player max-volume) player))
+ (pcase-let (((cl-struct listen-player max-volume) player)
+ (callback (lambda (msg)
+ (setf (listen-player-volume player) (map-elt msg
'data)))))
(if volume
(progn
(unless (<= 0 volume max-volume)
(error "VOLUME must be 0-%s" max-volume))
- (listen-mpv--set-property player "volume" volume))
- (listen-mpv--get-property player "volume"))))
+ (let ((request-id (listen-mpv--set-property player "volume" volume)))
+ (setf (map-elt (map-elt (listen-player-etc player) :requests)
request-id)
+ callback)
+ ;; We assume that the command will work, and we set the volume
that is being set,
+ ;; because the Transient description uses the value from the
player slot, and the
+ ;; callback can't make the Transient update itself.
+ (setf (listen-player-volume player) volume)))
+ (listen-player-volume player))))
+
+(cl-defmethod listen-mpv--update-property ((player listen-player-mpv) callback
property)
+ (let ((request-id (listen--send player "get_property" property)))
+ (setf (map-elt (map-elt (listen-player-etc player) :requests) request-id)
callback)))
(cl-defmethod listen-mpv--get-property ((player listen-player-mpv) property)
(pcase-let (((map error data) (listen--send player "get_property" property)))
@@ -204,10 +282,7 @@ VOLUME is an integer percentage."
(error nil))))))
(cl-defmethod listen-mpv--set-property ((player listen-player-mpv) property
&rest args)
- (pcase-let (((map error data) (apply #'listen--send player "set_property"
property args)))
- (pcase error
- ("success" data)
- (_ (error "listen-mpv--set-property: Setting property %S failed: %S"
property error)))))
+ (apply #'listen--send player "set_property" property args))
(provide 'listen-mpv)
diff --git a/listen.el b/listen.el
index 842094c030..db5cfc3fbd 100755
--- a/listen.el
+++ b/listen.el
@@ -244,19 +244,20 @@ Interactively, jump to current queue's current track."
(defun listen-mode-lighter ()
"Return lighter for `listen-mode'.
According to `listen-lighter-format', which see."
- (when-let ((listen-player)
- ((listen--running-p listen-player))
- ((listen--playing-p listen-player))
- (info (listen--info listen-player)))
+ (when-let* ((player listen-player)
+ ((listen--running-p player))
+ ((pcase (listen-player-status player)
+ ((or 'playing 'paused) t)))
+ (metadata (listen-player-metadata player)))
(format-spec listen-lighter-format
`((?a . ,(lambda ()
- (propertize (or (alist-get "artist" info nil nil
#'equal) "")
+ (propertize (or (alist-get 'artist metadata nil
nil #'equal) "")
'face 'listen-lighter-artist)))
(?A . ,(lambda ()
- (propertize (or (alist-get "album" info nil nil
#'equal) "")
+ (propertize (or (alist-get 'album metadata nil nil
#'equal) "")
'face 'listen-lighter-album)))
(?t . ,(lambda ()
- (if-let ((title (alist-get "title" info nil nil
#'equal)))
+ (if-let ((title (alist-get 'title metadata nil nil
#'equal)))
(propertize
(truncate-string-to-width title
listen-lighter-title-max-length
nil nil t)
@@ -272,9 +273,9 @@ According to `listen-lighter-format', which see."
'face 'listen-lighter-time)))
(?s . ,(lambda ()
(propertize (pcase (listen--status listen-player)
- ("playing" "▶")
- ("paused" "⏸")
- ("stopped" "■")
+ ('playing "▶")
+ ('paused "⏸")
+ ('stopped "■")
(_ ""))
'face 'bold)))
(?E . ,(lambda ()
@@ -302,15 +303,13 @@ According to `listen-lighter-format', which see."
(unless (or (listen--playing-p listen-player)
;; HACK: It seems that sometimes the player gets restarted
;; even when paused: this extra check should prevent that.
- (member (listen--status listen-player) '("playing"
"paused")))
+ (member (listen--status listen-player) '(playing paused)))
(setf playing-next-p
(run-hook-with-args 'listen-track-end-functions listen-player))))
(setf listen-mode-lighter
(when (and listen-player (listen--running-p listen-player))
(listen-mode-lighter)))
- (when playing-next-p
- ;; TODO: Remove this (I think it's not necessary anymore).
- (force-mode-line-update 'all))))
+ (force-mode-line-update 'all)))
(defun listen-play-next (player)
"Play PLAYER's queue's next track and return non-nil if playing."