branch: externals/listen commit ffb0a91371beed804db67e168320859aac8ea81d Author: Adam Porter <a...@alphapapa.net> Commit: Adam Porter <a...@alphapapa.net>
Add: (listen-mpv) MPV support Seems to work well so far. --- README.org | 2 + docs/README.org | 2 + listen-lib.el | 4 +- listen-mpv.el | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ listen.el | 10 ++++ 5 files changed, 183 insertions(+), 3 deletions(-) diff --git a/README.org b/README.org index d742680b8d..d1df928a03 100644 --- a/README.org +++ b/README.org @@ -226,6 +226,8 @@ The ~listen-mode~ minor mode runs a timer which plays the next track in the curr ** v0.10-pre *Additions* +- [[https://mpv.io/][MPV]] support. +- Option ~listen-backend~, which sets the backend to use: MPV or VLC. (The default is to auto-detect which is available at load time, with MPV being preferred due to more robust IPC support.) - Faces for parts of mode line lighter. *Fixes* diff --git a/docs/README.org b/docs/README.org index 63db70a7fb..398978af09 100644 --- a/docs/README.org +++ b/docs/README.org @@ -238,6 +238,8 @@ The ~listen-mode~ minor mode runs a timer which plays the next track in the curr ** v0.10-pre *Additions* ++ [[https://mpv.io/][MPV]] support. ++ Option ~listen-backend~, which sets the backend to use: MPV or VLC. (The default is to auto-detect which is available at load time, with MPV being preferred due to more robust IPC support.) + Faces for parts of mode line lighter. *Fixes* diff --git a/listen-lib.el b/listen-lib.el index 600afc5056..7287e774f4 100644 --- a/listen-lib.el +++ b/listen-lib.el @@ -139,12 +139,10 @@ return a list of values; otherwise return the sole value." ;;;; Functions -;; FIXME: Declare this differently or something. -(declare-function make-listen-player-vlc "listen-vlc") (defun listen-current-player () "Return variable `listen-player' or a newly set one if nil." (or listen-player - (setf listen-player (make-listen-player-vlc)))) + (setf listen-player (funcall (defvar listen-backend))))) (cl-defun listen-current-track (&optional (player listen-player)) "Return track playing on PLAYER, if any." diff --git a/listen-mpv.el b/listen-mpv.el new file mode 100755 index 0000000000..ecdb7c0638 --- /dev/null +++ b/listen-mpv.el @@ -0,0 +1,168 @@ +;;; listen-mpv.el --- MPV support for Emacs Music Player -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 Free Software Foundation, Inc. + +;; Author: Adam Porter <a...@alphapapa.net> + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; + +;;; Code: + +;;;; Requirements + +(require 'cl-lib) +(require 'map) + +(require 'listen-lib) + +;;;; Types + +(cl-defstruct + (listen-player-mpv + (:include listen-player + (command "mpv") + (args '("--no-msg-color" "--idle")) + (max-volume 100) + (etc '((:request-id . 0)))))) + +;;;; Functions + +(cl-defmethod listen--info ((player listen-player-mpv)) + (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)) + (listen-mpv--get-property player "metadata"))) + +(cl-defmethod listen--filename ((player listen-player-mpv)) + "Return filename of PLAYER's current track." + (let ((status (listen-mpv--get-property player "path" ))) + (when (string-match (rx bol "( new input: file://" (group (1+ nonl)) " )" ) status) + (match-string 1 status)))) + +(cl-defmethod listen--title ((player listen-player-mpv)) + (listen-mpv--get-property player "media-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))))) + (unless (process-live-p process) + (setf (listen-player-process player) + (apply #'start-process "listen-player-mpv" (generate-new-buffer " *listen-player-mpv*") + command args)) + (sleep-for 1) + (setf (map-elt (listen-player-etc player) :network-process) + (make-network-process :name "listen-player-mpv-socket" :family 'local + :remote socket + :buffer (generate-new-buffer " *listen-player-mpv-socket*"))) + (set-process-query-on-exit-flag (listen-player-process player) nil)))) + +(cl-defmethod listen--play ((player listen-player-mpv) file) + "Play FILE with PLAYER. +Stops playing, clears playlist, adds FILE, and plays it." + (listen--send player "loadfile" (expand-file-name file))) + +;; (cl-defmethod listen--stop ((player listen-player-mpv)) +;; "Stop playing with PLAYER." +;; (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"))) + +(cl-defmethod listen--pause ((player listen-player-mpv)) + "Pause playing with PLAYER." + (if (listen-mpv--get-property listen-player "pause") + (listen-mpv--set-property listen-player "pause" "no") + (listen-mpv--set-property listen-player "pause" "yes"))) + +(cl-defmethod listen--playing-p ((player listen-player-mpv)) + "Return non-nil if PLAYER is playing." + (not (listen-mpv--get-property player "idle-active"))) + +(cl-defmethod listen--elapsed ((player listen-player-mpv)) + "Return seconds elapsed for PLAYER's track." + (listen-mpv--get-property player "time-pos")) + +(cl-defmethod listen--length ((player listen-player-mpv)) + "Return length of PLAYER's track in seconds." + (listen-mpv--get-property player "duration")) + +(require 'json) + +(cl-defmethod listen--send ((player listen-player-mpv) command &rest args) + "Send COMMAND to PLAYER and return output." + (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 ((pos (marker-position (process-mark network-process))) + (json (json-encode + `(("command" ,command ,@args) + ("request_id" . ,request-id))))) + (process-send-string network-process json) + (process-send-string network-process "\n") + (with-local-quit + (accept-process-output network-process 2)) + ;; TODO: Consider using `json-read', etc. + (goto-char (point-min)) + (let ((json-false nil)) + (prog1 (cl-loop for result = (json-read) + while result + when (equal request-id (map-elt result 'request_id)) + return result) + (unless listen-debug-p + (erase-buffer)))))))) + +(cl-defmethod listen--seek ((player listen-player-mpv) seconds) + "Seek PLAYER to SECONDS." + (listen--send player "seek" seconds "absolute")) + +(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)) + (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")))) + +(cl-defmethod listen-mpv--get-property ((player listen-player-mpv) property) + (pcase-let (((map error data) (listen--send player "get_property" property))) + (pcase error + ("success" data) + (_ (error "listen-mpv--get-property: Getting property %S failed: %S" property error))))) + +(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))))) + +(provide 'listen-mpv) + +;;; listen-mpv.el ends here diff --git a/listen.el b/listen.el index 8b16f24941..e21f769f0b 100755 --- a/listen.el +++ b/listen.el @@ -60,6 +60,8 @@ (require 'map) (require 'listen-lib) +;; TODO: Can we load these as-needed? +(require 'listen-mpv) (require 'listen-vlc) ;;;; Variables @@ -119,6 +121,14 @@ Called with one argument, the player (if the player has a queue, its current track will be the one that just finished playing)." :type 'hook) +(defcustom listen-backend + (cond ((executable-find "mpv") #'make-listen-player-mpv) + ((executable-find "vlc") #'make-listen-player-vlc) + (t (display-warning 'listen-backend "Unable to find MPV or VLC." :error))) + "Player backend." + :type '(choice (const :tag "MPV" make-listen-player-mpv) + (const :tag "VLC" make-listen-player-vlc))) + ;;;; Commands (defun listen-quit (player)