diff options
| author | Magdalena Szweda <hi@roughlynamed.net> | 2026-10-06 19:57:36 +0200 |
|---|---|---|
| committer | Magdalena Szweda <hi@roughlynamed.net> | 2026-10-06 19:57:36 +0200 |
| commit | 6bfbb29b4bd4cf8a255c9ae803ef5dab061d2add (patch) | |
| tree | 3920cb1fb730e643f438005ab118cfbb2108a6f0 | |
| parent | 529ae845a1c33345d3399902c16a08c6dd3e7265 (diff) | |
h
| -rw-r--r-- | emms-player-xmms2.el | 216 |
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 |
