From 92df834ed9986f34a9a70ad84b4039be5e3a8ac1 Mon Sep 17 00:00:00 2001 From: Magdalena Szweda Date: Mon, 5 Oct 2026 19:46:01 +0200 Subject: EMMS(xmms2): Finish up emms-player-xmms2.el --- doom/.config/doom/config.el | 2 - doom/.config/doom/config.org | 147 +++++++++++++++++++++++++++++++++---------- 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: -- cgit v1.2.3