dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

mpv.el (13427B)


      1 ;;; mpv.el --- control mpv for easy note-taking  -*- lexical-binding: t; -*-
      2 
      3 ;; Copyright (C) 2014-2018  Johann Klähn
      4 
      5 ;; Author: Johann Klähn <johann@jklaehn.de>
      6 ;; URL: https://github.com/kljohann/mpv.el
      7 ;; Package-Version: 20211228.2043
      8 ;; Package-Commit: 4fd8baa508dbc1a6b42b4e40292c0dbb0f19c9b9
      9 ;; Version: 0.2.0
     10 ;; Keywords: tools, multimedia
     11 ;; Package-Requires: ((cl-lib "0.5") (emacs "25.1") (json "1.3") (org "8.0"))
     12 
     13 ;; This program is free software; you can redistribute it and/or modify
     14 ;; it under the terms of the GNU General Public License as published by
     15 ;; the Free Software Foundation, either version 3 of the License, or
     16 ;; (at your option) any later version.
     17 
     18 ;; This program is distributed in the hope that it will be useful,
     19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     21 ;; GNU General Public License for more details.
     22 
     23 ;; You should have received a copy of the GNU General Public License
     24 ;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
     25 
     26 ;;; Commentary:
     27 
     28 ;; This package is a potpourri of helper functions to control a mpv
     29 ;; process via its IPC interface.  You might want to add the following
     30 ;; to your init file:
     31 ;;
     32 ;; (org-add-link-type "mpv" #'mpv-play)
     33 ;; (defun org-mpv-complete-link (&optional arg)
     34 ;;   (replace-regexp-in-string
     35 ;;    "file:" "mpv:"
     36 ;;    (org-file-complete-link arg)
     37 ;;    t t))
     38 ;; (add-hook 'org-open-at-point-functions #'mpv-seek-to-position-at-point)
     39 
     40 ;;; Code:
     41 
     42 (require 'cl-lib)
     43 (require 'json)
     44 (require 'org)
     45 (require 'org-timer)
     46 (require 'tq)
     47 
     48 (defgroup mpv nil
     49   "Customization group for mpv."
     50   :prefix "mpv-"
     51   :group 'external)
     52 
     53 (defcustom mpv-executable "mpv"
     54   "Name or path to the mpv executable."
     55   :type 'file
     56   :group 'mpv)
     57 
     58 (defcustom mpv-default-options nil
     59   "List of default options to be passed to mpv."
     60   :type '(repeat string)
     61   :group 'mpv)
     62 
     63 (defcustom mpv-speed-step 1.10
     64   "Scale factor used when adjusting playback speed."
     65   :type 'number
     66   :group 'mpv)
     67 
     68 (defcustom mpv-volume-step 1.50
     69   "Scale factor used when adjusting volume."
     70   :type 'number
     71   :group 'mpv)
     72 
     73 (defcustom mpv-seek-step 5
     74   "Step size in seconds used when seeking."
     75   :type 'number
     76   :group 'mpv)
     77 
     78 (defcustom mpv-on-event-hook nil
     79   "Hook to run when an event message is received.
     80 The hook will be called with the parsed JSON message as its only an
     81 argument.  See \"List of events\" in the mpv man page."
     82   :type 'hook
     83   :group 'mpv)
     84 
     85 (defcustom mpv-on-start-hook nil
     86   "Hook to run when a new mpv process is started.
     87 The hook will be called with the arguments passed to `mpv-start'."
     88   :type 'hook
     89   :group 'mpv)
     90 
     91 (defcustom mpv-on-exit-hook nil
     92   "Hook to run when the mpv process dies."
     93   :type 'hook
     94   :group 'mpv)
     95 
     96 (defvar mpv--process nil)
     97 (defvar mpv--queue nil)
     98 
     99 (defun mpv-live-p ()
    100   "Return non-nil if inferior mpv is running."
    101   (and mpv--process (eq (process-status mpv--process) 'run)))
    102 
    103 (defun mpv-start (&rest args)
    104   "Start an mpv process with the specified ARGS.
    105 
    106 If there already is an mpv process controlled by this Emacs instance,
    107 it will be killed.  Options specified in `mpv-default-options' will be
    108 prepended to ARGS."
    109   (mpv-kill)
    110   (let ((socket (make-temp-name
    111                  (expand-file-name "mpv-" temporary-file-directory))))
    112     (setq mpv--process
    113           (apply #'start-process "mpv-player" nil mpv-executable
    114                  "--no-terminal"
    115                  (concat "--input-unix-socket=" socket)
    116                  (append mpv-default-options args)))
    117     (set-process-query-on-exit-flag mpv--process nil)
    118     (set-process-sentinel
    119      mpv--process
    120      (lambda (process _event)
    121        (when (memq (process-status process) '(exit signal))
    122          (mpv-kill)
    123          (when (file-exists-p socket)
    124            (with-demoted-errors (delete-file socket)))
    125          (run-hooks 'mpv-on-exit-hook))))
    126     (with-timeout
    127         (0.5 (mpv-kill)
    128              (error "Failed to connect to mpv"))
    129       (while (not (file-exists-p socket))
    130         (sleep-for 0.05)))
    131     (setq mpv--queue (tq-create
    132                   (make-network-process :name "mpv-socket"
    133                                         :family 'local
    134                                         :service socket)))
    135     (set-process-filter
    136      (tq-process mpv--queue)
    137      (lambda (_proc string)
    138        (mpv--tq-filter mpv--queue string)))
    139     (run-hook-with-args 'mpv-on-start-hook args)
    140     t))
    141 
    142 (defun mpv--as-strings (command)
    143   "Convert COMMAND to a list of strings."
    144   (mapcar (lambda (arg)
    145             (if (numberp arg)
    146                 (number-to-string arg)
    147               arg))
    148           command))
    149 
    150 (defun mpv--enqueue (command fn &optional delay-command)
    151   "Add COMMAND to the transaction queue.
    152 
    153 FN will be called with the corresponding answer.
    154 If DELAY-COMMAND is non-nil, delay sending this question until
    155 the process has finished replying to any previous questions.
    156 This produces more reliable results with some processes.
    157 
    158 Note that we do not use the regexp and closure arguments of
    159 `tq-enqueue', see our custom implementation of `tq-process-buffer'
    160 below."
    161   (when (mpv-live-p)
    162     (tq-enqueue
    163      mpv--queue
    164      (concat (json-encode `((command . ,(mpv--as-strings command)))) "\n")
    165      "" nil fn delay-command)
    166     t))
    167 
    168 (defun mpv-run-command (command &rest arguments)
    169   "Send a COMMAND to mpv, passing the remaining ARGUMENTS.
    170 Block while waiting for the response."
    171   (when (mpv-live-p)
    172     (let* ((response
    173             (cl-block mpv-run-command-wait-for-response
    174               (mpv--enqueue
    175                (cons command arguments)
    176                (lambda (response)
    177                  (cl-return-from mpv-run-command-wait-for-response
    178                    response)))
    179               (while (mpv-live-p)
    180                 (sleep-for 0.05))))
    181            (status (alist-get 'error response))
    182            (data (alist-get 'data response)))
    183     (unless (string-equal status "success")
    184       (error "`%s' failed: %s" command status))
    185     data)))
    186 
    187 (defun mpv--tq-filter (tq string)
    188   "Append to the queue's buffer and process the new data.
    189 
    190 TQ is a transaction queue created by `tq-create'.
    191 STRING is the data fragment received from the process.
    192 
    193 This is a verbatim copy of `tq-filter' that uses
    194 `mpv--tq-process-buffer' instead of `tq-process-buffer'."
    195   (let ((buffer (tq-buffer tq)))
    196     (when (buffer-live-p buffer)
    197       (with-current-buffer buffer
    198         (goto-char (point-max))
    199         (insert string)
    200         (mpv--tq-process-buffer tq)))))
    201 
    202 (defun mpv--tq-process-buffer (tq)
    203   "Check TQ's buffer for a JSON response.
    204 
    205 Replacement for `tq-process-buffer' that ignores regular expressions
    206 \(answers are always passed to the first handler in the queue) and
    207 passes unsolicited event messages to `mpv-on-event-hook'."
    208   (goto-char (point-min))
    209   (skip-chars-forward "^{")
    210   (let ((answer (ignore-errors (json-read))))
    211     (when answer
    212       (delete-region (point-min) (point))
    213       ;; event messages have form {"event": ...}
    214       ;; answers have form {"error": ..., "data": ...}
    215       (cond
    216        ((assoc 'event answer)
    217         (run-hook-with-args 'mpv-on-event-hook answer))
    218        ((not (tq-queue-empty tq))
    219         (unwind-protect
    220             (funcall (tq-queue-head-fn tq) answer)
    221           (tq-queue-pop tq))))
    222       ;; Recurse to check for further JSON messages.
    223       (mpv--tq-process-buffer tq))))
    224 
    225 ;;;###autoload
    226 (defun mpv-play (path)
    227   "Start an mpv process playing the file at PATH.
    228 
    229 You can use this with `org-add-link-type' or `org-file-apps'.
    230 See `mpv-start' if you need to pass further arguments and
    231 `mpv-default-options' for default options."
    232   (interactive "fFile: ")
    233   (mpv-start (expand-file-name path)))
    234 
    235 ;;;###autoload
    236 (defun mpv-kill ()
    237   "Kill the mpv process."
    238   (interactive)
    239   (when mpv--queue
    240     (tq-close mpv--queue))
    241   (when (mpv-live-p)
    242     (kill-process mpv--process))
    243   (with-timeout
    244       (0.5 (error "Failed to kill mpv"))
    245     (while (mpv-live-p)
    246       (sleep-for 0.05)))
    247   (setq mpv--process nil)
    248   (setq mpv--queue nil))
    249 
    250 ;;;###autoload
    251 (defun mpv-pause ()
    252   "Pause or unpause playback."
    253   (interactive)
    254   (mpv--enqueue '("cycle" "pause") #'ignore))
    255 
    256 (defun mpv-get-property (property)
    257   "Return the value of the given PROPERTY."
    258   (mpv-run-command "get_property" property))
    259 
    260 (defun mpv-set-property (property value)
    261   "Set the given PROPERTY to VALUE."
    262   (mpv-run-command "set_property" property value))
    263 
    264 (defun mpv-cycle-property (property)
    265   "Cycle the given PROPERTY."
    266   (mpv-run-command "cycle" property))
    267 
    268 (defun mpv-get-playback-position ()
    269   "Return the current playback position in seconds."
    270   (mpv-get-property "playback-time"))
    271 
    272 (defun mpv-get-duration ()
    273   "Return the estimated total duration of the current file in seconds."
    274   (mpv-get-property "duration"))
    275 
    276 ;;;###autoload
    277 (defun mpv-insert-playback-position (&optional arg)
    278   "Insert the current playback position at point.
    279 
    280 When called with a non-nil ARG, insert a timer list item like `org-timer-item'."
    281   (interactive "P")
    282   (let ((time (mpv-get-playback-position)))
    283     (funcall
    284      (if arg #'mpv--position-insert-as-org-item #'insert)
    285      (org-timer-secs-to-hms (round time)))))
    286 
    287 (defun mpv--position-insert-as-org-item (time-string)
    288   "Insert a description-type item with the playback position TIME-STRING.
    289 
    290 See `org-timer-item' which this is based on."
    291   (cl-letf (((symbol-function 'org-timer)
    292              (lambda (&optional _restart no-insert)
    293                (funcall
    294                 (if no-insert #'identity #'insert)
    295                 (concat time-string " ")))))
    296     (org-timer-item)))
    297 
    298 ;;;###autoload
    299 (defun mpv-seek-to-position-at-point ()
    300   "Jump to playback position as inserted by `mpv-insert-playback-position'.
    301 
    302 This can be used with the `org-open-at-point-functions' hook."
    303   (interactive)
    304   (save-excursion
    305     (skip-chars-backward ":[:digit:]" (point-at-bol))
    306     (when (looking-at "[0-9]+:[0-9]\\{2\\}:[0-9]\\{2\\}")
    307       (let ((secs (org-timer-hms-to-secs (match-string 0))))
    308         (when (>= secs 0)
    309           (mpv-seek secs))))))
    310 
    311 ;;;###autoload
    312 (defun mpv-speed-set (factor)
    313   "Set playback speed to FACTOR."
    314   (interactive "nFactor: ")
    315   (mpv--enqueue `("set" "speed" ,(abs factor)) #'ignore))
    316 
    317 ;;;###autoload
    318 (defun mpv-speed-increase (steps)
    319   "Increase playback speed by STEPS factors of `mpv-speed-step'."
    320   (interactive "p")
    321   (let ((factor (if (>= steps 0)
    322                     (* steps mpv-speed-step)
    323                   (/ 1 (* (- steps) mpv-speed-step)))))
    324     (mpv--enqueue `("multiply" "speed" ,factor) #'ignore)))
    325 
    326 ;;;###autoload
    327 (defun mpv-speed-decrease (steps)
    328   "Decrease playback speed by STEPS factors of `mpv-speed-step'."
    329   (interactive "p")
    330   (mpv-speed-increase (- steps)))
    331 
    332 ;;;###autoload
    333 (defun mpv-volume-set (factor)
    334   "Set playback volume to FACTOR."
    335   (interactive "nFactor: ")
    336   (mpv--enqueue `("set" "volume" ,(abs factor)) #'ignore))
    337 
    338 ;;;###autoload
    339 (defun mpv-volume-increase (steps)
    340   "Increase playback volume by STEPS factors of `mpv-volume-step'."
    341   (interactive "p")
    342   (let ((factor (if (>= steps 0)
    343                     (* steps mpv-volume-step)
    344                   (/ 1 (* (- steps) mpv-volume-step)))))
    345     (mpv--enqueue `("multiply" "volume" ,factor) #'ignore)))
    346 
    347 ;;;###autoload
    348 (defun mpv-volume-decrease (steps)
    349   "Decrease playback volume by STEPS factors of `mpv-volume-step'."
    350   (interactive "p")
    351   (mpv-volume-increase (- steps)))
    352 
    353 (defun mpv--raw-prefix-to-seconds (arg)
    354   "Convert raw prefix argument ARG to seconds using `mpv-seek-step'.
    355 Numeric arguments will be treated as seconds, repeated use
    356 \\[universal-argument] will be multiplied with `mpv-seek-step'."
    357   (if (numberp arg)
    358       arg
    359     (* mpv-seek-step
    360        (cl-signum (or (car arg) 1))
    361        (log (abs (or (car arg) 4)) 4))))
    362 
    363 ;;;###autoload
    364 (defun mpv-seek (seconds)
    365   "Seek to the given (absolute) time in SECONDS.
    366 A negative value is interpreted relative to the end of the file."
    367   (interactive "nPosition in seconds: ")
    368   (mpv--enqueue `("seek" ,seconds "absolute") #'ignore))
    369 
    370 ;;;###autoload
    371 (defun mpv-seek-forward (arg)
    372   "Seek forward ARG seconds.
    373 If ARG is numeric, it is used as the number of seconds.  Else each use
    374 of \\[universal-argument] will add another `mpv-seek-step' seconds."
    375   (interactive "P")
    376   (mpv--enqueue `("seek" ,(mpv--raw-prefix-to-seconds arg) "relative") #'ignore))
    377 
    378 ;;;###autoload
    379 (defun mpv-seek-backward (arg)
    380   "Seek backward ARG seconds.
    381 If ARG is numeric, it is used as the number of seconds.  Else each use
    382 of \\[universal-argument] will add another `mpv-seek-step' seconds."
    383   (interactive "P")
    384   (mpv-seek-forward (- (mpv--raw-prefix-to-seconds arg))))
    385 
    386 ;;;###autoload
    387 (defun mpv-revert-seek ()
    388   "Undo the previous seek command."
    389   (interactive)
    390   (mpv--enqueue '("revert-seek") #'ignore))
    391 
    392 ;;;###autoload
    393 (defun mpv-playlist-next ()
    394   "Go to the next entry on the playlist."
    395   (interactive)
    396   (mpv--enqueue '("playlist-next") #'ignore))
    397 
    398 ;;;###autoload
    399 (defun mpv-playlist-prev ()
    400   "Go to the previous entry on the playlist."
    401   (interactive)
    402   (mpv--enqueue '("playlist-prev") #'ignore))
    403 
    404 ;;;###autoload
    405 (defun mpv-version ()
    406   "Return the mpv version string.
    407 When called interactively, also show a more verbose version in
    408 the echo area."
    409   (interactive)
    410   (let ((version (cadr (split-string (car (process-lines mpv-executable "--version"))))))
    411     (prog1 version
    412       (if (called-interactively-p 'interactive)
    413 	  (message "mpv %s" version)))))
    414 
    415 (provide 'mpv)
    416 ;;; mpv.el ends here