blob: 350f0fa2387f0f5453cec1311a85d107be58d1dd (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
|
;;; emms-player-xmms2.el --- XMMS2 player for EMMS -*- lexical-binding: t; -*-
;; Setup:
;; (add-to-list 'load-path "/dir/containing/this/file")
;; (require 'emms-player-xmms2)
;; (setq emms-player-list '(emms-player-xmms2))
(require 'cl-lib)
(require 'dbus)
(require 'emms-player-simple)
(defgroup emms-player-xmms2 nil
"EMMS player for XMMS2."
:group 'emms-player
:prefix "emms-player-xmms2-")
(defcustom emms-player-xmms2-command "xmms2"
"The xmms2 CLI executable."
:type 'string)
(defcustom emms-player-xmms2-mpris-service "org.mpris.MediaPlayer2.xmms2"
"D-Bus name owned by the xmms2 MPRIS bridge."
:type 'string)
(defcustom emms-player-xmms2-check-interval 1
"Seconds between playback checks."
:type 'number)
(defcustom emms-player-xmms2
(emms-player #'emms-player-xmms2-start
#'emms-player-xmms2-stop
#'emms-player-xmms2-playable-p)
"Parameters for the XMMS2 player."
:type '(cons symbol alist))
(emms-player-set emms-player-xmms2 'regex
(emms-player-simple-regexp
"ogg" "flac" "mp3" "wav" "opus" "m4a" "aac" "mod"))
(emms-player-set emms-player-xmms2 'pause 'emms-player-xmms2-pause)
(emms-player-set emms-player-xmms2 'resume 'emms-player-xmms2-resume)
(emms-player-set emms-player-xmms2 'seek 'emms-player-xmms2-seek)
(emms-player-set emms-player-xmms2 'seek-to 'emms-player-xmms2-seek-to)
;;; State
(defvar emms-player-xmms2--timer nil)
(defvar emms-player-xmms2--seen-playing nil
"Non-nil once XMMS2 has reported Playing/Paused since the last start.
Guards against reading a stale \"Stopped\" right after starting playback.")
(defvar emms-player-xmms2--synced nil
"File names last pushed into XMMS2's playlist, in order.")
(defvar emms-player-xmms2--tracks nil
"EMMS tracks mirrored in XMMS2's playlist, in the same order.")
(defvar emms-player-xmms2--pos nil
"1-based position XMMS2 was last known to be on.")
;;; Helpers
(defun emms-player-xmms2--run (&rest args)
"Run the xmms2 CLI with ARGS. Return non-nil on success."
(zerop (apply #'call-process emms-player-xmms2-command nil nil nil args)))
(defun emms-player-xmms2--state ()
"Return the MPRIS PlaybackStatus string, or nil if unreachable."
(ignore-errors
(dbus-get-property :session emms-player-xmms2-mpris-service
"/org/mpris/MediaPlayer2"
"org.mpris.MediaPlayer2.Player" "PlaybackStatus")))
(defun emms-player-xmms2--list-info ()
"Return (CURRENT-POS . COUNT) parsed from `xmms2 list', or nil on failure.
CURRENT-POS is the 1-based position of the `->' entry, or nil."
(with-temp-buffer
(when (zerop (call-process emms-player-xmms2-command nil t nil "list"))
(let ((count 0) (pos nil))
(goto-char (point-min))
(while (re-search-forward
"^\\(->\\| \\)\\[\\([0-9]+\\)/[0-9]+\\]" nil t)
(setq count (1+ count))
(when (string= (match-string 1) "->")
(setq pos (string-to-number (match-string 2)))))
(cons pos count)))))
(defun emms-player-xmms2--name (track)
"The file name to hand to xmms2 for TRACK."
(expand-file-name (emms-track-name track)))
(defun emms-player-xmms2--playlist-tracks ()
"Playable tracks of the current EMMS playlist, in order."
(with-current-emms-playlist
(cl-remove-if-not
#'emms-player-xmms2-playable-p
(save-excursion
(nreverse (emms-playlist-tracks-in-region (point-min) (point-max)))))))
(defun emms-player-xmms2--select (track)
"Move the EMMS selection to TRACK in the current playlist buffer."
(with-current-emms-playlist
(goto-char (point-min))
(let (found)
(while (and (not found) (not (eobp)))
(if (eq (ignore-errors (emms-playlist-track-at (point))) track)
(setq found (point))
(forward-line 1)))
(when found (emms-playlist-select found)))))
(defun emms-player-xmms2--cancel-timer ()
(when emms-player-xmms2--timer
(cancel-timer emms-player-xmms2--timer)
(setq emms-player-xmms2--timer nil)))
;;; Following XMMS2
(defun emms-player-xmms2--advanced (pos)
"XMMS2 moved on to position POS by itself"
(setq emms-player-xmms2--pos pos)
(let ((track (nth (1- pos) emms-player-xmms2--tracks)))
(when track
(let ((emms-player-stopped-p t)) ; don't let EMMS pick the next track
(emms-player-stopped))
(emms-player-xmms2--select track)
(emms-player-started 'emms-player-xmms2))))
(defun emms-player-xmms2--went-idle (pos)
"XMMS2 stopped without asking. POS is its marker position"
(emms-player-xmms2--cancel-timer)
(setq emms-player-xmms2--seen-playing nil)
(if (or (null pos) (>= pos (length emms-player-xmms2--tracks)))
(emms-player-stopped)
(let ((emms-player-stopped-p t))
(emms-player-stopped))))
(defun emms-player-xmms2--poll ()
(let ((state (emms-player-xmms2--state))
(pos (car (emms-player-xmms2--list-info))))
(cond ((member state '("Playing" "Paused"))
(setq emms-player-xmms2--seen-playing t)
(when (and pos (not (eql pos emms-player-xmms2--pos)))
(emms-player-xmms2--advanced pos)))
((and emms-player-xmms2--seen-playing (equal state "Stopped"))
(emms-player-xmms2--went-idle pos)))))
;;; EMMS API
(defun emms-player-xmms2-playable-p (track)
"Return non-nil when can play TRACK."
(and (eq (emms-track-type track) 'file)
(string-match (emms-player-get emms-player-xmms2 'regex)
(emms-track-name track))
(executable-find emms-player-xmms2-command)
t))
(defun emms-player-xmms2-start (track)
"Play TRACK, syncing XMMS2's playlist with EMMS's first if needed."
(emms-player-xmms2--cancel-timer)
(setq emms-player-xmms2--seen-playing nil)
(let* ((tracks (emms-player-xmms2--playlist-tracks))
(index (cl-position track tracks :test #'eq)))
(unless index
(setq tracks (list track) index 0))
(let* ((names (mapcar #'emms-player-xmms2--name tracks))
(info (emms-player-xmms2--list-info)))
(unless (and (equal names emms-player-xmms2--synced)
info (= (cdr info) (length names)))
(emms-player-xmms2--run "clear")
(apply #'emms-player-xmms2--run "add" "-f" names)
(setq emms-player-xmms2--synced names))
(setq emms-player-xmms2--tracks tracks
emms-player-xmms2--pos (1+ index))
(if (and (emms-player-xmms2--run "jump" (number-to-string (1+ index)))
(emms-player-xmms2--run "play"))
(progn
(emms-player-started 'emms-player-xmms2)
(setq emms-player-xmms2--timer
(run-at-time emms-player-xmms2-check-interval
emms-player-xmms2-check-interval
#'emms-player-xmms2--poll)))
(message "XMMS2: could not start %s" (car names))))))
(defun emms-player-xmms2-stop ()
"Stop playback."
(interactive)
(emms-player-xmms2--cancel-timer)
(setq emms-player-xmms2--seen-playing nil)
(emms-player-xmms2--run "stop")
(let ((emms-player-stopped-p t))
(emms-player-stopped)))
(defun emms-player-xmms2-pause ()
"Pause playback."
(interactive)
(emms-player-xmms2--run "pause"))
(defun emms-player-xmms2-resume ()
"Resume playback."
(interactive)
(emms-player-xmms2--run "play"))
(defun emms-player-xmms2-seek (amount)
"Seek by AMOUNT seconds."
(interactive "nSeconds: ")
(emms-player-xmms2--run "seek" (format "%+d" (round amount))))
(defun emms-player-xmms2-seek-to (pos)
"Seek to POS seconds from the start of the track."
(interactive "nSeconds: ")
(emms-player-xmms2--run "seek" (number-to-string (round pos))))
(defun emms-player-xmms2-resync ()
"Force the next start to rebuild XMMS2's playlist from EMMS's."
(interactive)
(setq emms-player-xmms2--synced nil))
(provide 'emms-player-xmms2)
;;; emms-player-xmms2.el ends here
|