;;; 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