aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMagdalena Szweda <hi@roughlynamed.net>2026-10-05 19:46:01 +0200
committerMagdalena Szweda <hi@roughlynamed.net>2026-10-05 21:35:27 +0200
commit92df834ed9986f34a9a70ad84b4039be5e3a8ac1 (patch)
treeab62392fd44744221dcab590496f366a8ecaae92
parent07fa1a67d1988a9a1d9044898216f57153f91668 (diff)
EMMS(xmms2): Finish up emms-player-xmms2.el
-rw-r--r--doom/.config/doom/config.el2
-rw-r--r--doom/.config/doom/config.org147
2 files changed, 114 insertions, 35 deletions
diff --git a/doom/.config/doom/config.el b/doom/.config/doom/config.el
index 2f08da8..6554431 100644
--- a/doom/.config/doom/config.el
+++ b/doom/.config/doom/config.el
@@ -1,5 +1,3 @@
-;;; $DOOMDIR/config.el -*- lexical-binding: t; -*-
-
(load! "splash")
(defun +splash-initial-buffer ()
(splash-screen)
diff --git a/doom/.config/doom/config.org b/doom/.config/doom/config.org
index f633aac..28d95bb 100644
--- a/doom/.config/doom/config.org
+++ b/doom/.config/doom/config.org
@@ -1,4 +1,6 @@
#+TITLE: Literate DOOM Emacs configuration
+#+begin_src emacs-lisp :tangle ~/.config/doom/emms-player-xmms2.el
+;;; emms-player-xmms2.el --- XMMS2 player for EMMS -*- lexical-binding: t; -*-
Emacs is a great operating system.
But every operating system has 1 essential line of code.
@@ -518,9 +520,8 @@ I plan on getting this into Savannah, then GNU ELPA, or having it merged with th
And now, ladies & gentlemen, i present to you a bunch of self-documenting code im too lazy to split up into different source blocks!!!
#+begin_src emacs-lisp :tangle ~/.config/doom/emms-player-xmms2.el
-;;; emms-player-xmms2.el --- XMMS2 player for EMMS (stage 1 - unfinished) -*- lexical-binding: t; -*-
-;; Copyright (C) 2026 Magdalena Szweda
-;; Author: Magdalena Szweda
+;;; 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)
@@ -544,7 +545,7 @@ And now, ladies & gentlemen, i present to you a bunch of self-documenting code i
:type 'string)
(defcustom emms-player-xmms2-check-interval 1
- "Seconds between playback-state checks."
+ "Seconds between playback checks."
:type 'number)
(defcustom emms-player-xmms2
@@ -555,22 +556,30 @@ And now, ladies & gentlemen, i present to you a bunch of self-documenting code i
:type '(cons symbol alist))
(emms-player-set emms-player-xmms2 'regex
- (concat "\\`http[s]?://\\|"
- (emms-player-simple-regexp
- "ogg" "flac" "mp3" "wav" "opus" "m4a" "aac" "mod")))
+ (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)
-;;; Internals
+;;; State
(defvar emms-player-xmms2--timer nil)
(defvar emms-player-xmms2--seen-playing nil
- "Non-nil once XMMS2 has reported Playing/Paused for the current track.")
+ "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. "
+ "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 ()
@@ -580,49 +589,115 @@ And now, ladies & gentlemen, i present to you a bunch of self-documenting code i
"/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 ()
- "Tell EMMS when XMMS2 finishes the track on its own."
- (let ((state (emms-player-xmms2--state)))
+ (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))
+ (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--cancel-timer)
- (setq emms-player-xmms2--seen-playing nil)
- ;; stopped-p is nil here, so EMMS advances to the next track
- (emms-player-stopped)))))
+ (emms-player-xmms2--went-idle pos)))))
;;; EMMS API
(defun emms-player-xmms2-playable-p (track)
- "Return non-nil when we can play TRACK."
- (and (memq (emms-track-type track) '(file url))
+ "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: clear XMMS2's playlist, add it, play."
+ "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 ((name (emms-track-name track)))
- (when (eq (emms-track-type track) 'file)
- (setq name (expand-file-name name)))
- (if (and (emms-player-xmms2--run "clear")
- (emms-player-xmms2--run "add" name)
- (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" name))))
+ (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."
@@ -653,9 +728,15 @@ And now, ladies & gentlemen, i present to you a bunch of self-documenting code i
(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
+
#+end_src
#+RESULTS: