fordon_greeman / voidfiles

Branch: main
Switch branches/tags
main
theidioticdev add emacs 2079ec5 23h ago
772 lines · 30.2 KB Raw Blame History
1 ;;; emms-player-mpv.el --- mpv support for EMMS -*- lexical-binding: t; -*-
2 ;;
3 ;; Copyright (C) 2018-2024 Free Software Foundation, Inc.
4
5 ;; Authors: Mike Kazantsev <mk.fraggod@gmail.com>
6
7 ;; This file is part of EMMS.
8
9 ;; EMMS is free software; you can redistribute it and/or
10 ;; modify it under the terms of the GNU General Public License
11 ;; as published by the Free Software Foundation; either version 3
12 ;; of the License, or (at your option) any later version.
13
14 ;; EMMS is distributed in the hope that it will be useful,
15 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 ;; GNU General Public License for more details.
18
19 ;; You should have received a copy of the GNU General Public License
20 ;; along with EMMS; if not, write to the Free Software Foundation,
21 ;; Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
22
23 ;;; Commentary:
24
25 ;;
26 ;; This code provides EMMS backend for using mpv player.
27 ;;
28 ;; It works using long-running mpv instance and its JSON IPC interface
29 ;; to switch tracks and receive player feedback/metadata.
30 ;;
31 ;; In default configuration, mpv will read its configuration files
32 ;; (see its manpage for locations), and can display window for
33 ;; video, subtitles, album-art or audio visualization.
34 ;;
35 ;; Useful `emms-player-mpv-parameters' tweaks:
36 ;;
37 ;; - Ignore config file(s): (add-to-list 'emms-player-mpv-parameters "--no-config")
38 ;; - Disable vo window: (add-to-list 'emms-player-mpv-parameters "--vo=null")
39 ;; - Show simple cqt visualizer window:
40 ;; (add-to-list 'emms-player-mpv-parameters
41 ;; "--lavfi-complex=[aid1]asplit[ao][a]; [a]showcqt[vo]")
42 ;;
43 ;; See "M-x customize-group emms-player-mpv" and mpv manpage for more options.
44 ;;
45 ;; See `emms-player-mpv-event-connect-hook' and `emms-player-mpv-event-functions',
46 ;; as well as `emms-player-mpv-ipc-req-send' for handling more mpv events,
47 ;; processing more playback info and metadata from it, as well as extending
48 ;; control over its vast functionality.
49 ;;
50
51 ;;; Code:
52
53
54 (require 'emms)
55 (require 'emms-player-simple)
56 (require 'emms-playing-time)
57 (require 'json)
58 (require 'cl-lib)
59
60
61 (defgroup emms-player-mpv nil
62 "EMMS player for mpv."
63 :group 'emms-player
64 :prefix "emms-player-mpv-")
65
66 (defcustom emms-player-mpv
67 (emms-player
68 #'emms-player-mpv-start
69 #'emms-player-mpv-stop
70 #'emms-player-mpv-playable-p)
71 "*Parameters for mpv player."
72 :type '(cons symbol alist))
73
74 (emms-player-set emms-player-mpv 'regex
75 (apply #'emms-player-simple-regexp emms-player-base-format-list))
76
77 (defcustom emms-player-mpv-command-name "mpv"
78 "mpv binary to use. Can be absolute path or just binary name."
79 :type 'file)
80
81 (defcustom emms-player-mpv-parameters
82 '("--quiet" "--really-quiet" "--no-audio-display")
83 "Extra command-line arguments for started mpv process(es).
84 Either a list of strings or function returning such list.
85 Extra arguments --idle and --input-ipc-server are added automatically.
86 Note that unless --no-config option is specified here,
87 mpv will also use options from its configuration files.
88 For mpv binary path, see `emms-player-mpv-command-name'."
89 :type '(choice (repeat :tag "List of mpv arguments" string)
90 function))
91
92 (defcustom emms-player-mpv-environment ()
93 "List of extra environment variables (\"VAR=value\" strings) to pass on to
94 mpv process.
95
96 These are added on top of `process-environment' by default.
97 Adding nil as an element to this list will discard emacs
98 `process-environment' and only pass variables that are specified
99 in the list."
100 :type '(repeat (choice string (const :tag "Start from blank environment" nil))))
101
102 (defcustom emms-player-mpv-ipc-socket
103 (concat (file-name-as-directory emms-directory)
104 "mpv-ipc.sock")
105 "Unix socket path to use with mpv --input-ipc-socket= option."
106 :type 'file)
107
108 (defvar emms-player-mpv-ipc-proc nil) ; to avoid warnings while keeping useful defs at the top
109
110 (defcustom emms-player-mpv-update-duration t
111 "Update track duration when played by mpv.
112 Uses `emms-player-mpv-event-functions' hook."
113 :type 'boolean
114 :set (lambda (sym value)
115 (set-default-toplevel-value sym value)
116 (run-at-time 0.1 nil
117 (lambda (value)
118 (if value
119 (add-hook
120 'emms-player-mpv-event-functions
121 #'emms-player-mpv-info-duration-event-func)
122 (remove-hook
123 'emms-player-mpv-event-functions
124 #'emms-player-mpv-info-duration-event-func)))
125 value)))
126
127 (defcustom emms-player-mpv-update-metadata nil
128 "Update track info (artist, album, name, etc) from mpv events, when it
129 is played.
130
131 This allows to dynamically update stream info from ICY tags, for
132 example. Uses `emms-player-mpv-event-connect-hook' and
133 `emms-player-mpv-event-functions' hooks."
134 :type 'boolean
135 :set (lambda (sym value)
136 (set-default-toplevel-value sym value)
137 (run-at-time 0.1 nil
138 (lambda (value)
139 (if value
140 (progn
141 (add-hook
142 'emms-player-mpv-event-connect-hook
143 #'emms-player-mpv-info-meta-connect-func)
144 (add-hook
145 'emms-player-mpv-event-functions
146 #'emms-player-mpv-info-meta-event-func)
147 (when (process-live-p emms-player-mpv-ipc-proc)
148 (emms-player-mpv-info-meta-connect-func)))
149 (progn
150 (remove-hook
151 'emms-player-mpv-event-connect-hook
152 #'emms-player-mpv-info-meta-connect-func)
153 (remove-hook
154 'emms-player-mpv-event-functions
155 #'emms-player-mpv-info-meta-event-func))))
156 value)))
157
158 (defcustom emms-player-mpv-use-playlist-option nil
159 "Use --playlist option and loadlist mpv command for playlist files and URLs.
160
161 Use of this option is explicitly discouraged by mpv documentation for security
162 reasons, and should be unnecessary in most common cases with modern mpv.
163 Make sure to check mpv manpage for --playlist option before enabling this."
164 :type 'boolean)
165
166
167 (defvar emms-player-mpv-proc nil
168 "Running mpv process, controlled over --input-ipc-server unix socket.")
169
170 (defvar emms-player-mpv-proc-kill-delay 5
171 "Delay until SIGKILL gets sent to `emms-player-mpv-proc',
172 if it refuses to exit cleanly on `emms-player-mpv-proc-stop'.")
173
174
175 (defvar emms-player-mpv-ipc-proc nil
176 "Unix socket network process connected to running `emms-player-mpv-proc'
177 instance.")
178
179 (defvar emms-player-mpv-ipc-buffer " *emms-player-mpv-ipc*"
180 "Buffer to associate with `emms-player-mpv-ipc-proc' socket process.")
181
182 (defvar emms-player-mpv-ipc-connect-timer nil
183 "Timer for connection attempts to JSON IPC unix socket.")
184 (defvar emms-player-mpv-ipc-connect-delays
185 '(0.1 0.1 0.1 0.1 0.1 0.1 0.2 0.2 0.3 0.3 0.5 1.0 1.0 2.0)
186 "List of delays before initiating socket connection for new mpv process.")
187
188 (defvar emms-player-mpv-ipc-connect-command nil
189 "JSON command for `emms-player-mpv-ipc-sentinel' to run when it connects to mpv.
190 I.e. last command that either initiated connection or was used while
191 connecting to mpv.
192 Set by `emms-player-mpv-start' and such,
193 cleared once it gets sent by `emms-player-mpv-ipc-sentinel'.")
194
195 (defvar emms-player-mpv-ipc-id 1
196 "Auto-incremented counter for unique JSON request identifiers.
197 Use for for `request_id' and `observe_property' identifiers.
198 Use `emms-player-mpv-ipc-id-get' to get and increment this value,
199 instead of using it directly.
200 Wraps-around upon reaching `emms-player-mpv-ipc-id-max'
201 (unlikely to ever happen).")
202
203 (defvar emms-player-mpv-ipc-id-max (expt 2 30)
204 "Max value for `emms-player-mpv-ipc-id' to wrap around after.
205 Should be fine with both mpv and Emacs, and probably never reached anyway.")
206
207 (defvar emms-player-mpv-ipc-req-table nil
208 "Auto-initialized hash table of outstanding API req_ids to their handler funcs.")
209
210 (defvar emms-player-mpv-ipc-stop-command nil
211 "Internal flag to track when stop command starts/finishes before next loadfile.
212 Set to either nil, t or the playback start function to call on end-file event
213 after stop command.
214 This is a workaround for mpv-0.30+ behavior, where \"stop + loadfile\" only
215 runs \"stop\".")
216
217
218 (defvar emms-player-mpv-event-connect-hook nil
219 "Normal hook run right after establishing new JSON IPC connection to mpv.
220 Runs before `emms-player-mpv-ipc-connect-command', if any.
221 Best place to send any `observe_property', `request_log_messages',
222 `enable_event' commands.
223 Use `emms-player-mpv-ipc-id-get' to get unique id values for these.
224 See also `emms-player-mpv-event-functions'.")
225
226 (defvar emms-player-mpv-event-functions nil
227 "List of functions to call for each event emitted from JSON IPC.
228 One argument is passed to each function - JSON line,
229 as sent by mpv and decoded by `json-read-from-string'.
230 See also `emms-player-mpv-event-connect-hook'.")
231
232
233 (defvar emms-player-mpv-stopped nil
234 "Non-nil if playback was stopped by call from emms.
235 Similar to `emms-player-stopped-p', but set for future async events,
236 to indicate that playback should stop instead of switching to next track.")
237
238 (defvar emms-player-mpv-idle-timer (timer-create)
239 "Timer to delay `emms-player-stopped' when mpv unexpectedly goes idle.")
240
241 (defvar emms-player-mpv-idle-delay 0.5
242 "Delay before issuing `emms-player-stopped' when mpv unexpectedly goes idle.")
243
244
245 (make-obsolete 'emms-player-mpv-ipc-method nil "Emms 18")
246
247 (defcustom emms-player-mpv-ipc-method nil
248 "Unused obsolete value. It was used for selecting older IPC methods.
249 Haven't been needed since mpv 0.17.0 (2016-04-11), removed in Emms 18+ (2024)."
250 :type 'symbol)
251
252
253 ;; ----- helpers
254
255 (defvar emms-player-mpv-debug nil
256 "Enable to print sent/received JSON lines and process
257 start/stop events to *Messages* buffer using `emms-player-mpv-debug-msg'.")
258
259 (defvar emms-player-mpv-debug-ts-offset nil
260 "Timestamp offset for `emms-player-mpv-debug-msg'.
261 Set on first use, with intent to both shorten and obfuscate time in logs.")
262
263 (defun emms-player-mpv-debug-trim (s)
264 (if (stringp s)
265 (replace-regexp-in-string "\\(^[ \t\n\r]+\\|[ \t\n\r]+$\\)" "" s t t)
266 s))
267
268 (defun emms-player-mpv-debug-msg (tpl-or-msg &rest tpl-values)
269 "Print debug message to *Messages* if `emms-player-mpv-debug' is non-nil.
270 Message is only formatted if TPL-VALUES is non-empty.
271 Strips whitespace from start/end of TPL-OR-MSG and strings in TPL-VALUES."
272 (when emms-player-mpv-debug
273 (setq
274 tpl-or-msg (emms-player-mpv-debug-trim tpl-or-msg)
275 tpl-values (seq-map #'emms-player-mpv-debug-trim tpl-values))
276 (unless tpl-values
277 (setq tpl-or-msg (replace-regexp-in-string "%" "%%" tpl-or-msg t t)))
278 (let ((ts (float-time)))
279 (unless emms-player-mpv-debug-ts-offset (setq emms-player-mpv-debug-ts-offset ts))
280 (apply #'message
281 (concat "emms-player-mpv %.1f " tpl-or-msg)
282 (- ts emms-player-mpv-debug-ts-offset)
283 tpl-values))))
284
285
286 ;; ----- mpv process
287
288 (defun emms-player-mpv-proc-playing-p (&optional proc)
289 "Return whether playback in PROC or `emms-player-mpv-proc' is started,
290 which is distinct from \"start-command sent\" and \"process is running\" states.
291 Used to signal emms via `emms-player-started' and `emms-player-stopped' calls."
292 (let ((proc (or proc emms-player-mpv-proc)))
293 (and proc (process-get proc 'mpv-playing))))
294
295 (defun emms-player-mpv-proc-playing (state &optional proc)
296 "Set process mpv-playing state flag for `emms-player-mpv-proc-playing-p'."
297 (let ((proc (or proc emms-player-mpv-proc)))
298 (when proc (process-put proc 'mpv-playing state))))
299
300 (defun emms-player-mpv-proc-symbol-id (sym &optional proc)
301 "Get unique process-specific id integer for SYM or nil if it
302 was already requested."
303 (let
304 ((proc (or proc emms-player-mpv-proc))
305 (sym-id (intern (concat "mpv-sym-" (symbol-name sym)))))
306 (unless (process-get proc sym-id)
307 (let ((id (emms-player-mpv-ipc-id-get)))
308 (process-put proc sym-id id)
309 id))))
310
311 (defun emms-player-mpv-proc-sentinel (proc ev)
312 (let
313 ((status (process-status proc))
314 (playing (emms-player-mpv-proc-playing-p proc)))
315 (emms-player-mpv-debug-msg
316 "proc[%s]: %s (status=%s, playing=%s)" proc ev status playing)
317 (when (and (memq status '(exit signal))
318 playing)
319 (emms-player-stopped))))
320
321 (defun emms-player-mpv-proc-init (&rest media-args)
322 "initialize new mpv process as `emms-player-mpv-proc'.
323 MEDIA-ARGS are used instead of --idle, if specified."
324 (emms-player-mpv-proc-stop)
325 (unless (file-directory-p (file-name-directory emms-player-mpv-ipc-socket))
326 (make-directory (file-name-directory emms-player-mpv-ipc-socket)))
327 (let*
328 ((argv emms-player-mpv-parameters)
329 (argv (append
330 (list emms-player-mpv-command-name)
331 (if (functionp argv)
332 (funcall argv)
333 argv)
334 (list (format "--input-ipc-server=%s" emms-player-mpv-ipc-socket))
335 (or media-args '("--idle"))))
336 (env emms-player-mpv-environment)
337 (process-environment (append
338 (unless (seq-some 'not env)
339 process-environment)
340 (seq-filter 'identity env))))
341 (setq emms-player-mpv-proc
342 (make-process :name "emms-player-mpv"
343 :buffer nil :command argv :noquery t :sentinel #'emms-player-mpv-proc-sentinel))
344 (emms-player-mpv-debug-msg "proc[%s]: start %s" emms-player-mpv-proc argv)))
345
346 (defun emms-player-mpv-proc-stop ()
347 "Stop running `emms-player-mpv-proc' instance via SIGINT, if any.
348
349 `delete-process' (SIGKILL) timer is started if
350 `emms-player-mpv-proc-kill-delay' is non-nil."
351 (when emms-player-mpv-proc
352 (let ((proc emms-player-mpv-proc))
353 (emms-player-mpv-debug-msg "proc[%s]: stop" proc)
354 (if (not (process-live-p proc))
355 (delete-process proc)
356 (emms-player-mpv-proc-playing nil proc)
357 (interrupt-process proc)
358 (when emms-player-mpv-proc-kill-delay
359 (run-at-time
360 emms-player-mpv-proc-kill-delay nil
361 (lambda (proc)
362 (delete-process proc))
363 proc))))
364 (setq emms-player-mpv-proc nil)))
365
366
367 ;; ----- IPC unix socket
368
369 (defun emms-player-mpv-ipc-sentinel (proc ev)
370 (emms-player-mpv-debug-msg "ipc[%s]: %s" proc ev)
371 (when (memq (process-status proc)
372 '(open run))
373 (run-hooks 'emms-player-mpv-event-connect-hook)
374 (when emms-player-mpv-ipc-connect-command
375 (let ((cmd emms-player-mpv-ipc-connect-command))
376 (setq emms-player-mpv-ipc-connect-command nil)
377 (emms-player-mpv-ipc-req-send cmd nil proc)))))
378
379 (defun emms-player-mpv-ipc-filter (proc s)
380 (when (buffer-live-p (process-buffer proc))
381 (with-current-buffer (process-buffer proc)
382 (let ((moving (= (point)
383 (process-mark proc))))
384 (save-excursion
385 (goto-char (process-mark proc))
386 (insert s)
387 (set-marker (process-mark proc)
388 (point)))
389 (if moving (goto-char (process-mark proc))))
390 ;; Process/remove all complete lines of json, if any
391 (let ((p0 (point-min)))
392 (while
393 (progn
394 (goto-char p0)
395 (end-of-line)
396 (equal (following-char)
397 ?\n))
398 (let*
399 ((p1 (point))
400 (json (buffer-substring p0 p1)))
401 (delete-region p0 (+ p1 1))
402 (emms-player-mpv-ipc-recv json)))))))
403
404 (defun emms-player-mpv-ipc-connect (delays)
405 "Make IPC connection attempt, rescheduling if there's no socket by (car DELAYS).
406 (cdr DELAYS) gets passed to next connection attempt,
407 so it can be rescheduled further until function runs out of DELAYS values.
408 Sets `emms-player-mpv-ipc-proc' value to resulting process on success."
409 (emms-player-mpv-debug-msg "ipc: connect-delay %s" (car delays))
410 (setq emms-player-mpv-ipc-proc
411 (condition-case nil
412 (make-network-process
413 :name "emms-player-mpv-ipc"
414 :family 'local
415 :service emms-player-mpv-ipc-socket
416 :nowait t
417 :coding '(utf-8 . utf-8)
418 :buffer (get-buffer-create emms-player-mpv-ipc-buffer)
419 :noquery t
420 :filter #'emms-player-mpv-ipc-filter
421 :sentinel #'emms-player-mpv-ipc-sentinel)
422 (file-error nil)))
423 (unless (process-live-p emms-player-mpv-ipc-proc)
424 (setq emms-player-mpv-ipc-proc nil))
425 (when (and (not emms-player-mpv-ipc-proc)
426 delays)
427 (run-at-time (car delays)
428 nil #'emms-player-mpv-ipc-connect (cdr delays))))
429
430 (defun emms-player-mpv-ipc-init ()
431 "Initialize new mpv ipc socket/file process and associated state."
432 (emms-player-mpv-ipc-stop)
433 (emms-player-mpv-debug-msg "ipc: init")
434 (when emms-player-mpv-ipc-connect-timer (cancel-timer emms-player-mpv-ipc-connect-timer))
435 (with-current-buffer (get-buffer-create emms-player-mpv-ipc-buffer) (erase-buffer))
436 (setq
437 emms-player-mpv-ipc-id 1
438 emms-player-mpv-ipc-req-table nil
439 emms-player-mpv-ipc-connect-timer nil
440 emms-player-mpv-ipc-connect-timer
441 (run-at-time (car emms-player-mpv-ipc-connect-delays)
442 nil
443 #'emms-player-mpv-ipc-connect (cdr emms-player-mpv-ipc-connect-delays))))
444
445 (defun emms-player-mpv-ipc-stop ()
446 (when emms-player-mpv-ipc-proc
447 (emms-player-mpv-debug-msg "ipc: stop")
448 (delete-process emms-player-mpv-ipc-proc)
449 (setq emms-player-mpv-ipc-proc nil)))
450
451 (defun emms-player-mpv-ipc ()
452 "Return open IPC process or nil, (re-)starting mpv/connection if necessary.
453
454 Return nil when starting async process/connection, and any
455 follow-up command should be stored to
456 `emms-player-mpv-ipc-connect-command' in this case."
457 (unless (process-live-p emms-player-mpv-proc)
458 (emms-player-mpv-proc-init))
459 (unless (process-live-p emms-player-mpv-ipc-proc)
460 (emms-player-mpv-ipc-init))
461 (and
462 emms-player-mpv-ipc-proc
463 (memq (process-status emms-player-mpv-ipc-proc) '(open run))
464 emms-player-mpv-ipc-proc))
465
466
467 ;; ----- IPC protocol
468
469 (defun emms-player-mpv-ipc-id-get ()
470 "Get new connection-unique id value, tracked via `emms-player-mpv-ipc-id'."
471 (let ((ipc-id emms-player-mpv-ipc-id))
472 (setq emms-player-mpv-ipc-id
473 (if (< emms-player-mpv-ipc-id emms-player-mpv-ipc-id-max)
474 (1+ emms-player-mpv-ipc-id)
475 1))
476 ipc-id))
477
478 (defun emms-player-mpv-ipc-req-send (cmd &optional handler proc)
479 "Send JSON IPC request and assign HANDLER to response for it, if any.
480
481 CMD value is encoded via `json-encode'.
482
483 HANDLER func will be called with decoded response JSON
484 as (handler data err), where ERR will be either nil on
485 \"success\", \"connection-error\" or whatever is in JSON. If
486 HANDLER is nil, default `emms-player-mpv-ipc-req-error-printer'
487 will be used to at least log errors. Multiple commands can be
488 batched in one list as \\='(batch (cmd1 . handler1) ...), in which
489 case common HANDLER argument is ignored. PROC can be specified
490 to avoid `emms-player-mpv-ipc' call (e.g. from sentinel/filter
491 funcs)."
492 (dolist
493 (cmd-and-handler
494 (if (and (listp cmd)
495 (eq (car cmd)
496 'batch))
497 (cdr cmd)
498 `((,cmd . ,handler))))
499 (cl-destructuring-bind (cmd . handler)
500 cmd-and-handler
501 (let
502 ((req-id (emms-player-mpv-ipc-id-get))
503 (req-proc (or proc (emms-player-mpv-ipc)))
504 (handler (or handler #'emms-player-mpv-ipc-req-error-printer)))
505 (unless emms-player-mpv-ipc-req-table
506 (setq emms-player-mpv-ipc-req-table (make-hash-table)))
507 (let ((json (concat (json-encode (list :command cmd :request_id req-id))
508 "\n")))
509 (emms-player-mpv-debug-msg "json >> %s" json)
510 (condition-case _err
511 ;; On any disconnect, assume that mpv process is to blame and force restart.
512 (process-send-string req-proc json)
513 (error
514 (emms-player-mpv-proc-stop)
515 (funcall handler nil 'connection-error)
516 (setq handler nil))))
517 (when handler (puthash req-id handler emms-player-mpv-ipc-req-table))))))
518
519 (defun emms-player-mpv-ipc-req-resolve (req-id data err)
520 "Run handler-func for specified req-id."
521 (when emms-player-mpv-ipc-req-table
522 (let
523 ((handler (gethash req-id emms-player-mpv-ipc-req-table))
524 (err (if (string= err "success")
525 nil err)))
526 (remhash req-id emms-player-mpv-ipc-req-table)
527 (when handler (funcall handler data err)))))
528
529 (defun emms-player-mpv-ipc-req-error-printer (_data err)
530 "Simple default `emms-player-mpv-ipc-req-send' handler to log
531 errors, if any."
532 (when err (message "emms-player-mpv ipc-error: %s" err)))
533
534 (defun emms-player-mpv-ipc-recv (json)
535 "Handler for all JSON lines from mpv process."
536 (emms-player-mpv-debug-msg "json << %s" json)
537 (let*
538 ((json-data (json-read-from-string json))
539 (req-id (alist-get 'request_id json-data))
540 (ev (alist-get 'event json-data)))
541 (when req-id
542 ;; Response to command
543 (emms-player-mpv-ipc-req-resolve req-id
544 (alist-get 'data json-data)
545 (alist-get 'error json-data)))
546 (when ev
547 ;; mpv event
548 (emms-player-mpv-event-handler json-data)
549 (run-hook-with-args 'emms-player-mpv-event-functions json-data))))
550
551 (defun emms-player-mpv-observe-property (sym)
552 "Send mpv observe_property command for property identified by SYM.
553 Only sends command once per process, removing any
554 potential duplication if used for same properties from different functions."
555 (let ((id (emms-player-mpv-proc-symbol-id sym)))
556 (when id (emms-player-mpv-ipc-req-send `(observe_property ,id ,sym)))))
557
558 (defun emms-player-mpv-event-idle ()
559 "Delayed check for switching tracks when mpv goes idle for no good reason."
560 (emms-player-mpv-debug-msg "idle-check (stopped=%s)" emms-player-mpv-stopped)
561 (unless emms-player-mpv-stopped (emms-player-stopped)))
562
563 (defun emms-player-mpv-event-playing-time-sync ()
564 "Request and update `emms-playing-time' after playback
565 seek/restart or unpause."
566 (emms-player-mpv-ipc-req-send '(get_property time-pos)
567 #'(lambda (pos err)
568 (unless err (emms-playing-time-set pos)))))
569
570 (defun emms-player-mpv-event-handler (json-data)
571 "Handler for supported mpv events, including property changes.
572
573 Called before `emms-player-mpv-event-functions' and does same
574 thing as these hooks."
575 (pcase (alist-get 'event json-data)
576 ("playback-restart"
577 ;; Separate emms-player-mpv-proc-playing state is used for emms started/stopped signals,
578 ;; because start-file/end-file are also emitted after track-change and for playlists,
579 ;; and don't correspond to actual playback state.
580 (unless (emms-player-mpv-proc-playing-p)
581 (emms-player-mpv-proc-playing t)
582 (emms-player-started emms-player-mpv))
583 (emms-player-mpv-event-playing-time-sync))
584 ("pause"
585 (unless emms-player-paused-p
586 (setq emms-player-paused-p t)
587 (run-hooks 'emms-player-paused-hook)))
588 ("unpause"
589 (emms-player-mpv-event-playing-time-sync)
590 (when emms-player-paused-p
591 (setq emms-player-paused-p nil)
592 (run-hooks 'emms-player-paused-hook)))
593 ("end-file"
594 (when (emms-player-mpv-proc-playing-p)
595 (emms-player-mpv-proc-playing nil)
596 (emms-player-stopped))
597 (when emms-player-mpv-ipc-stop-command
598 (unless (eq emms-player-mpv-ipc-stop-command t)
599 (funcall emms-player-mpv-ipc-stop-command))
600 (setq emms-player-mpv-ipc-stop-command nil)))
601 ("idle"
602 ;; Can mean any kind of error before or during playback.
603 ;; Example can be access/format error, resulting in start+end without playback-restart.
604 (cancel-timer emms-player-mpv-idle-timer)
605 (setq
606 emms-player-mpv-idle-timer
607 (run-at-time emms-player-mpv-idle-delay nil #'emms-player-mpv-event-idle)
608 emms-player-mpv-ipc-stop-command nil))
609 ("start-file" (cancel-timer emms-player-mpv-idle-timer))))
610
611
612 ;; ----- Metadata update hooks
613
614 (defun emms-player-mpv-info-meta-connect-func ()
615 "Hook function for `emms-player-mpv-event-connect-hook' to update
616 metadata from mpv."
617 (emms-player-mpv-observe-property 'metadata)
618 (emms-player-mpv-observe-property 'duration))
619
620 (defun emms-player-mpv-info-meta-event-func (json-data)
621 "Hook function for `emms-player-mpv-event-functions' to update
622 metadata from mpv."
623 (when
624 (and
625 (string= (alist-get 'event json-data)
626 "property-change")
627 (string= (alist-get 'name json-data)
628 "metadata"))
629 (let ((info-alist (alist-get 'data json-data)))
630 (when info-alist (emms-player-mpv-info-meta-update-track info-alist)))))
631
632 (defun emms-player-mpv-info-meta-update-track (info-alist &optional track)
633 "Update TRACK with mpv metadata from INFO-ALIST.
634 `emms-playlist-current-selected-track' is used by default."
635 (mapc
636 (lambda (cc)
637 (setcar cc (intern (downcase (symbol-name (car cc))))))
638 info-alist)
639 (cl-macrolet
640 ((key (k)
641 `(alist-get ',k info-alist))
642 (set-track-info (track &rest body)
643 (cons 'progn
644 (cl-loop for (k v)
645 on body by 'cddr collect
646 `(let ((value ,v))
647 (when value
648 (emms-track-set ,track ',(intern (format "info-%s" k))
649 value)))))))
650 (unless track (setq track (emms-playlist-current-selected-track)))
651 (set-track-info track
652 title (or (key title)
653 (unless (string= "" (key icy-title)) (key icy-title))
654 (key icy-name))
655 artist (or (key artist)
656 (key album_artist)
657 (key icy-name))
658 album (key album)
659 tracknumber (key track)
660 year (key date)
661 genre (key genre)
662 note (key comment))
663 (emms-track-updated track)))
664
665 (defun emms-player-mpv-info-duration-event-func (json-data)
666 "Hook function for `emms-player-mpv-event-functions' to update
667 track duration from mpv."
668 (when
669 (and
670 (string= (alist-get 'event json-data) "property-change")
671 (string= (alist-get 'name json-data) "duration"))
672 (let
673 ((duration (alist-get 'data json-data))
674 (track (emms-playlist-current-selected-track)))
675 (when (and track (numberp duration) (> duration 0))
676 (setq duration (round duration))
677 (emms-track-set track 'info-playing-time duration)
678 (emms-track-set track 'info-playing-time-min (/ duration 60))
679 (emms-track-set track 'info-playing-time-sec (% duration 60))))))
680
681
682 ;; ----- High-level EMMS interface
683
684 (defun emms-player-mpv-cmd (cmd &optional handler)
685 "Send mpv command to process/connection if both are running,
686 or otherwise schedule start/connect and set
687 `emms-player-mpv-ipc-connect-command' for `emms-player-mpv-ipc-sentinel'.
688 Multiple commands can be batched in one list as \\='(batch (cmd1 . handler1) ...),
689 in which case common HANDLER argument is ignored."
690 (setq emms-player-mpv-ipc-connect-command nil)
691 (let ((proc (emms-player-mpv-ipc)))
692 (if proc
693 (emms-player-mpv-ipc-req-send cmd handler proc)
694 (setq emms-player-mpv-ipc-connect-command cmd))))
695
696 (defmacro emms-player-mpv-cmd-prog (cmd &rest handler-body)
697 "Obsolete macro around `emms-player-mpv-cmd' that creates handler
698 callback (see `emms-player-mpv-ipc-req-send') from HANDLER-BODY
699 forms, which have following bindings:
700
701 - mpv-cmd for CMD.
702 - mpv-data for response data (decoded json, nil if none).
703 - mpv-error for response error (nil if no error, decoded json or
704 \\='connection-error).
705
706 Do not use it with new code - it will raise warnings when used
707 with lexical bindings, and will be removed in a future EMMS
708 version."
709 `(emms-player-mpv-cmd ,cmd (apply-partially
710 (lambda (mpv-cmd mpv-data mpv-error)
711 ,@handler-body)
712 ,cmd)))
713
714 (make-obsolete 'emms-player-mpv-cmd-prog nil "Emms 7")
715
716
717 (defun emms-player-mpv-playable-p (track)
718 (memq (emms-track-type track)
719 '(file url streamlist playlist)))
720
721 (defun emms-player-mpv-start (track)
722 (setq emms-player-mpv-stopped nil)
723 (emms-player-mpv-proc-playing nil)
724 (let*
725 ((track-name (emms-track-get track 'name))
726 (track-playlist-option
727 (and emms-player-mpv-use-playlist-option
728 (memq (emms-track-get track 'type)
729 '(streamlist playlist))))
730 (play-cmd `(batch
731 ((,(if track-playlist-option 'loadlist 'loadfile)
732 ,track-name replace))
733 ((set pause no))))
734 (start-func
735 ;; Try running play-cmd and retry it on conn failure, e.g. if mpv died
736 (apply-partially 'emms-player-mpv-cmd play-cmd
737 (lambda (_mpv-data mpv-error)
738 (when (eq mpv-error 'connection-error)
739 (emms-player-mpv-cmd play-cmd))))))
740 (if emms-player-mpv-ipc-stop-command
741 (setq emms-player-mpv-ipc-stop-command start-func)
742 (funcall start-func))))
743
744 (defun emms-player-mpv-stop ()
745 (setq
746 emms-player-mpv-stopped t
747 emms-player-mpv-ipc-stop-command t)
748 (emms-player-mpv-proc-playing nil)
749 (emms-player-mpv-cmd `(stop))
750 (emms-player-stopped))
751
752
753 (defun emms-player-mpv-pause ()
754 (emms-player-mpv-cmd `(set pause yes)))
755
756 (defun emms-player-mpv-resume ()
757 (emms-player-mpv-cmd `(set pause no)))
758
759 (defun emms-player-mpv-seek (sec)
760 (emms-player-mpv-cmd `(seek ,sec relative)))
761
762 (defun emms-player-mpv-seek-to (sec)
763 (emms-player-mpv-cmd `(seek ,sec absolute)))
764
765 (emms-player-set emms-player-mpv 'pause #'emms-player-mpv-pause)
766 (emms-player-set emms-player-mpv 'resume #'emms-player-mpv-resume)
767 (emms-player-set emms-player-mpv 'seek #'emms-player-mpv-seek)
768 (emms-player-set emms-player-mpv 'seek-to #'emms-player-mpv-seek-to)
769
770
771 (provide 'emms-player-mpv)
772 ;;; emms-player-mpv.el ends here