From 6cadabfd28326ec266f93c140b4632a75fbb210b Mon Sep 17 00:00:00 2001 From: john muhl Date: Sun, 15 Sep 2024 19:52:25 -0500 Subject: [PATCH] Add notifications support to 'mpc' (Bug#73538) * lisp/mpc.el (mpc-notifications, mpc-notifications-title) (mpc-notifications-body): New option. (mpc--notifications-id): New variable. (mpc-notifications-notify, mpc-cover-image-find) (mpc-cover-image-p, mpc--notifications-format): New function. (mpc-format): Use 'mpc-cover-find' and expand docstring to include details about the FORMAT-SPEC. (mpc-status-callbacks): Add file callback for notifications. (cherry picked from commit 49084bad7990a614bdd3ea7a24ebab0fc89627e3) --- etc/NEWS | 8 +++++ lisp/mpc.el | 96 ++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 96 insertions(+), 8 deletions(-) diff --git a/etc/NEWS b/etc/NEWS index bb80e4170a5..303d4aee4b0 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -491,6 +491,14 @@ instead. This command widens the view to the current fold level when in a fold, or behaves like 'widen' if not in a fold. +** MPC + +--- +*** New user option 'mpc-notifications'. +When non-nil MPC displays a desktop notification when the song changes. +The notification’s title and body can be customized with +'mpc-notifications-title' and 'mpc-notifications-body'. + * New Modes and Packages in Emacs 31.1 diff --git a/lisp/mpc.el b/lisp/mpc.el index 751d5a95178..0d0a1685f4f 100644 --- a/lisp/mpc.el +++ b/lisp/mpc.el @@ -95,6 +95,8 @@ (require 'cl-lib) (require 'subr-x)) +(require 'notifications) + (defgroup mpc () "Client for the Music Player Daemon (mpd)." :prefix "mpc-" @@ -456,6 +458,7 @@ which will be concatenated with proper quoting before passing them to MPD." (state . mpc--faster-toggle-refresh) ;Only ffwd/rewind while play/pause. (volume . mpc-volume-refresh) (file . mpc-songpointer-refresh) + (file . mpc-notifications-notify) ;; The song pointer may need updating even if the file doesn't change, ;; if the same song appears multiple times in a row. (song . mpc-songpointer-refresh) @@ -947,6 +950,20 @@ If PLAYLIST is t or nil or missing, use the main playlist." ;; aux) )) +(defun mpc-cover-image-find (file) + "Find cover image for FILE." + (and-let* ((default-directory mpc-mpd-music-directory) + (dir (mpc-file-local-copy (file-name-directory file))) + (files (directory-files dir)) + (cover (seq-find #'mpc-cover-image-p files)) + ((expand-file-name cover dir))))) + +(defun mpc-cover-image-p (file) + "Check if FILE is a cover image." + (let ((covers '(".folder.png" "folder.png" "cover.jpg" "folder.jpg"))) + (or (seq-find (lambda (cover) (string= file cover)) covers) + (and mpc-cover-image-re (string-match-p mpc-cover-image-re file))))) + ;;; Formatter ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defcustom mpc-cover-image-re nil ; (rx (or ".jpg" ".jpeg" ".png") string-end) @@ -981,7 +998,15 @@ If PLAYLIST is t or nil or missing, use the main playlist." (push file mpc-tempfiles)) (defun mpc-format (format-spec info &optional hscroll) - "Format the INFO according to FORMAT-SPEC, inserting the result at point." + "Format the INFO according to FORMAT-SPEC, inserting the result at point. + +FORMAT-SPEC is a string of the format '%-WIDTH{NAME-POST}' where the first +'-', WIDTH and -POST are optional. % followed by the optional '-' means +to right align the output. WIDTH limits the output to the specified +number of characters by replacing any further output with a horizontal +ellipsis. The optional -POST means to use the empty string if NAME is +absent or else use the concatenation of the content of NAME with the +string POST." (let* ((pos 0) (start (point)) (col (if hscroll (- hscroll) 0)) @@ -1015,7 +1040,8 @@ If PLAYLIST is t or nil or missing, use the main playlist." (substring time (match-end 0)) time))))) ('Cover - (let ((dir (file-name-directory (cdr (assq 'file info))))) + (let* ((file (alist-get 'file info)) + (dir (file-name-directory file))) ;; (debug) (setq pred ;; We want the closure to capture the current @@ -1026,12 +1052,7 @@ If PLAYLIST is t or nil or missing, use the main playlist." (and (funcall oldpred info) (equal dir (file-name-directory (cdr (assq 'file info)))))))) - (if-let* ((covers '(".folder.png" "folder.png" "cover.jpg" "folder.jpg")) - (cover (cl-loop for file in (directory-files (mpc-file-local-copy dir)) - if (or (member (downcase file) covers) - (and mpc-cover-image-re - (string-match mpc-cover-image-re file))) - return (concat dir file))) + (if-let* ((cover (mpc-cover-image-find file)) (file (with-demoted-errors "MPC: %s" (mpc-file-local-copy cover)))) (let (image) @@ -2755,6 +2776,65 @@ If stopped, start playback." (t (error "Unsupported drag'n'drop gesture")))))) +;;; Notifications ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(declare-function notifications-notify "notifications") + +(defcustom mpc-notifications nil + "Non-nil means to display notifications when the song changes." + :version "31.1" + :type 'boolean) + +(defcustom mpc-notifications-title + '("%{Title}" "Unknown Title") + "FORMAT-SPEC used in the notification title. + +The first element that returns a non-emtpy string is used. The last +element is a plain string to use as fallback for when none of the tags +are found. See `mpc-format' for the definition of FORMAT-SPEC." + :version "31.1" + :type '(repeat string)) + +(defcustom mpc-notifications-body + '("%{Artist}" "%{AlbumArtist}" "Unknown Artist") + "FORMAT-SPEC used in the notification body. + +The first element that returns a non-emtpy string is used. The last +element is a plain string to use as fallback for when none of the tags +are found. See `mpc-format' for the definition of FORMAT-SPEC." + :version "31.1" + :type '(repeat string)) + +(defvar mpc--notifications-id nil) + +(defun mpc--notifications-format (format-specs) + "Use FORMAT-SPECS to get string for use in notification." + (seq-some + (lambda (spec) + (let ((text (with-temp-buffer + (mpc-format spec mpc-status) + (buffer-string)))) + (if (string= "" text) nil text))) + format-specs)) + +(defun mpc-notifications-notify () + "Display a notification with information about the current song." + (when-let ((mpc-notifications) + ((notifications-get-server-information)) + ((string= "play" (alist-get 'state mpc-status))) + (title (mpc--notifications-format mpc-notifications-title)) + (body (mpc--notifications-format mpc-notifications-body)) + (icon (or (mpc-cover-image-find (alist-get 'file mpc-status)) + notifications-application-icon))) + (setq mpc--notifications-id + (notifications-notify :title title + :body body + :app-icon icon + :replaces-id mpc--notifications-id)))) + + + + ;;; Toplevel ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defcustom mpc-frame-alist '((name . "MPC") (tool-bar-lines . 1) -- 2.39.2