;;; jao-mpc.el --- Using mpc to interact with mpd -*- lexical-binding: t; -*- ;; Copyright (C) 2021, 2022 jao ;; Author: jao ;; Keywords: convenience ;; Version: 0.1 ;; Package-requires: ((emacs "27.1")) ;; URL: https://codeberg.org/jao/lib/media ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; Simple mpd interaction using mpc. ;;; Code: (require 'jao-themes) (require 'jao-lyrics) (require 'jao-random-album) (defconst jao-mpc--albums "*MPC Albums*") (defconst jao-mpc--playlist "*MPC Playlist*") (defvar jao-mpc-port 6600) (defvar-local jao-mpc--port nil) (defun jao-mpc--cmd (cmd &optional port) (let ((port (or port jao-mpc--port jao-mpc-port))) (shell-command-to-string (format "mpc -p %s %s" port cmd)))) (defconst jao-mpc--fields '(artist album composer originaldate genre title track position time name)) (defconst jao-mpc--stfmt (mapconcat (lambda (f) (format "%s:::%%%s%%" f f)) jao-mpc--fields "\n")) (defun jao-mpc--current (&optional port) (let ((s (jao-mpc--cmd (format "-f '%s' current" jao-mpc--stfmt) port)) (res)) (dolist (s (split-string s "\n" t " ") res) (when (string-match "\\(.+\\):::\\(.+\\)" s) (push (cons (intern (match-string 1 s)) (match-string 2 s)) res))))) (defun jao-mpc--playing-p (&optional port) (not (string-blank-p (jao-mpc--cmd "status|grep '\\[playing\\]'" port)))) (defun jao-mpc--queue-len (&optional port) (string-to-number (jao-mpc--cmd "playlist|wc -l" port))) (defsubst jao--put-face (str face) (put-text-property 0 (length str) 'face face str) str) (defun jao-mpc--current-str (&optional port current len) (let* ((current (or current (jao-mpc--current port))) (len (or len (jao-mpc--queue-len port))) (title (alist-get 'title current (alist-get 'name current ""))) (album (alist-get 'album current)) (artist (alist-get 'artist current)) (composer (alist-get 'composer current)) (no (string-to-number (alist-get 'position current "0"))) (time (alist-get 'time current ""))) (format " %s%s %s%s%s%s" (jao--put-face (if (zerop no) "" (format "%02d/%s " no len)) 'jao-themes-f02) (jao--put-face title 'jao-themes-f00) (jao--put-face artist 'jao-themes-f01) (jao--put-face (if composer (format " [%s]" composer) "") 'jao-themes-f01) (jao--put-face (if album (format " (%s)" album) "") 'jao-themes-f11) (if (string-blank-p time) "" (jao--put-face (format " [%s]" time) 'jao-themes-dimm))))) (defvar jao-mpc-minibuffer-str "") (defun jao-mpc--set-current-str (&optional port) (setq jao-mpc-minibuffer-str (if (jao-mpc--playing-p port) (jao-mpc--current-str port) (when (and (null port) jao-random-album-p (not (jao-mpc--current))) (jao-random-album-next)) "")) (jao-minibuffer-refresh)) (defvar jao-mpc--idle-procs nil) (defun jao-mpc--idle-loop (&optional port) (when-let (proc (alist-get port jao-mpc--idle-procs)) (ignore-errors (kill-process proc))) (setf (alist-get port jao-mpc--idle-procs nil t) (make-process :name (format "jao-mpc-idleloop (%s)" port) :buffer nil :noquery t :command `("mpc" "-p" ,(format "%s" (or port jao-mpc-port)) "idleloop" "player") :filter (lambda (_p _s) (jao-mpc--set-current-str port))))) (define-derived-mode jao-mpc-albums-mode fundamental-mode "MPC Albums" "Mode to display the list of albums known by mpd." (read-only-mode -1) (delete-region (point-min) (point-max)) (insert (jao-mpc--cmd "list album")) (goto-char (point-min)) (read-only-mode 1)) (defun jao-mpc--album-buffer () (if-let (b (get-buffer jao-mpc--albums)) b (with-current-buffer (get-buffer-create jao-mpc--albums) (jao-mpc-albums-mode) (current-buffer)))) (defun jao-mpc--add-and-play (&optional album) (interactive) (let ((album (or album (string-trim (thing-at-point 'line))))) (jao-mpc--cmd "clear") (jao-mpc--cmd (format "findadd album \"%s\"" album)) (jao-mpc--cmd "play"))) (define-key jao-mpc-albums-mode-map (kbd "n") #'next-line) (define-key jao-mpc-albums-mode-map (kbd "p") #'previous-line) (define-key jao-mpc-albums-mode-map (kbd "RET") #'jao-mpc--add-and-play) (define-key jao-mpc-albums-mode-map (kbd "q") #'bury-buffer) (define-derived-mode jao-mpc-playlist-mode nil "MPC Playlist" "Mode to display the list of playlist known by mpd." (read-only-mode -1) (delete-region (point-min) (point-max)) (setq-local jao-mpc--port jao-mpc-port) (insert (jao-mpc--cmd "playlist")) (goto-char (point-min)) (display-line-numbers-mode 1) (read-only-mode 1)) (defun jao-mpc--playlist-goto-current () (interactive) (let ((c (string-trim (or (jao-mpc--cmd "current") "")))) (unless (string-blank-p c) (goto-char (point-min)) (when (re-search-forward (regexp-quote c) nil t) (beginning-of-line))))) (defun jao-mpc--playlist-play () (interactive) (jao-mpc--cmd (format "play %s" (line-number-at-pos)))) (define-key jao-mpc-playlist-mode-map (kbd "n") #'next-line) (define-key jao-mpc-playlist-mode-map (kbd "p") #'previous-line) (define-key jao-mpc-playlist-mode-map (kbd "q") #'bury-buffer) (define-key jao-mpc-playlist-mode-map (kbd ".") #'jao-mpc--playlist-goto-current) (define-key jao-mpc-playlist-mode-map (kbd "RET") #'jao-mpc--playlist-play) (define-key jao-mpc-playlist-mode-map (kbd "C") #'jao-mpc-clear) (defun jao-mpc--playlist-buffer (&optional port) (with-current-buffer (get-buffer-create jao-mpc--playlist) (let ((jao-mpc-port (or port jao-mpc-port))) (jao-mpc-playlist-mode)) (current-buffer))) ;;;###autoload (defun jao-mpc-stop (&optional port) (interactive) (jao-mpc--cmd "stop" port)) ;;;###autoload (defun jao-mpc-toggle (&optional port) (interactive) (jao-mpc--cmd "toggle" port)) ;;;###autoload (defun jao-mpc-play (&optional port) (interactive) (jao-mpc--cmd "play" port)) ;;;###autoload (defun jao-mpc-next (&optional port) (interactive) (jao-mpc--cmd "next" port)) ;;;###autoload (defun jao-mpc-previous (&optional port) (interactive) (jao-mpc--cmd "prev" port)) ;;;###autoload (defun jao-mpc-seek (delta &optional port) (interactive "nDelta: ") (jao-mpc--cmd (format "seek %s%s" (if (> delta 0) "+" "") delta) port)) ;;;###autoload (defun jao-mpc-clear (&optional port) (interactive) (jao-mpc--cmd "clear" port)) ;;;###autoload (defun jao-mpc-echo-current (&optional port) (interactive) (jao-notify (jao-mpc--current-str port))) ;;;###autoload (defun jao-mpc-add-url (url) (interactive "sURL: ") (jao-mpc--cmd (format "add %s" url))) ;;;###autoload (defun jao-mpc-show-albums () "Show album list." (interactive) (pop-to-buffer (jao-mpc--album-buffer))) ;;;###autoload (defun jao-mpc-show-playlist (&optional port) "Show current playlist." (interactive) (pop-to-buffer (jao-mpc--playlist-buffer port)) (jao-mpc--playlist-goto-current)) ;;;###autoload (defun jao-mpc-lyrics-track-data (&optional port) (let ((c (string-trim (jao-mpc--cmd "current" port)))) (unless (string-blank-p c) (when (string-match "\\(.+\\) - \\(.+\\)" c) (cons (match-string 1 c) (match-string 2 c)))))) ;;;###autoload (defun jao-mpc-connect (&optional port) (interactive) (jao-mpc--idle-loop port) (when (jao-mpc--playing-p port) (jao-mpc--set-current-str port))) ;;;###autoload (defun jao-mpc-setup (&optional secondary-port priority) (setq jao-lyrics-info-function #'jao-mpc-lyrics-track-data) (jao-random-album-setup #'jao-mpc--album-buffer #'jao-mpc--add-and-play #'jao-mpc-stop jao-notify-audio-icon) (jao-mpc-connect) (when secondary-port (jao-mpc-connect secondary-port)) (when priority (if (> priority 0) (jao-minibuffer-add-variable 'jao-mpc-minibuffer-str priority) (jao-minibuffer-add-msg-variable 'jao-mpc-minibuffer-str (- priority))))) (defvar jao-mpc--album-titles nil) (defconst jao-mpc--albums-cmd "-f '%album% - %artist%' find \"(ALBUM =~ '.*')\" | uniq") ;;;###autoload (defun jao-mpc-select-album (refresh) (interactive "P") (let ((albums (or (and (not refresh) jao-mpc--album-titles) (setq jao-mpc--album-titles (split-string (jao-mpc--cmd jao-mpc--albums-cmd) "\n" t))))) (when-let (album (completing-read "Play album: " albums nil t)) (jao-mpc--add-and-play (car (split-string album "-" t " ")))))) (provide 'jao-mpc) ;;; jao-mpc.el ends here