aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMagdalena Szweda <hi@roughlynamed.net>2026-10-06 19:57:36 +0200
committerMagdalena Szweda <hi@roughlynamed.net>2026-10-06 19:57:36 +0200
commit6bfbb29b4bd4cf8a255c9ae803ef5dab061d2add (patch)
tree3920cb1fb730e643f438005ab118cfbb2108a6f0
parent529ae845a1c33345d3399902c16a08c6dd3e7265 (diff)
h
-rw-r--r--emms-player-xmms2.el216
1 files changed, 216 insertions, 0 deletions
diff --git a/emms-player-xmms2.el b/emms-player-xmms2.el
new file mode 100644
index 0000000..350f0fa
--- /dev/null
+++ b/emms-player-xmms2.el
@@ -0,0 +1,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