;;; 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)) (r (shell-command-to-string (format "mpc -p %s %s" port cmd)))) (replace-regexp-in-string "^\\(warning: \\)?MPD .+\n" "" r))) (defun jao-mpc--fformat (fields) (mapconcat (lambda (f) (format "%s:::%%%s%%" f f)) fields "\n")) (defconst jao-mpc--fields '(artist album composer originaldate genre title track position time name)) (defconst jao-mpc--stfmt (jao-mpc--fformat '(artist album composer originaldate genre title track name))) (defconst jao-mpc--stfmtt (jao-mpc--fformat '(currenttime totaltime percenttime songpos length))) (defmacro jao-mpc--parse-fields (res-str res) `(dolist (s (split-string ,res-str "\n" t " ") ,res) (when (string-match "\\(.+\\):::\\(.+\\)" s) (push (cons (intern (match-string 1 s)) (match-string 2 s)) ,res)))) (defun jao-mpc--current (&optional port) (let ((s (jao-mpc--cmd (format "-f '%s' current" jao-mpc--stfmt) port)) (st (jao-mpc--cmd (format "status '%s'" jao-mpc--stfmtt))) (res)) (jao-mpc--parse-fields s res) (jao-mpc--parse-fields st res))) (defun jao-mpc--playing-p (&optional port) (string-prefix-p "playing" (jao-mpc--cmd "status %state%" port))) (defun jao-mpc--queue-len (&optional port) (string-to-number (jao-mpc--cmd "status %length%" port))) (defsubst jao--put-face (str face) (put-text-property 0 (length str) 'face face str) str) (defun jao-mpc--current-timestr (times &optional current) (let* ((current (or current (jao-mpc--current))) (time (alist-get 'totaltime current ""))) (if times (format "%s/%s%s" (alist-get 'currenttime current "") time (alist-get 'percenttime current "")) (format "%s" time)))) (defun jao-mpc--current-str (&optional port times) (if-let* ((current (jao-mpc--current port)) (title (alist-get 'title current (alist-get 'name current)))) (let ((len (alist-get 'length current "0")) (album (alist-get 'album current)) (artist (alist-get 'artist current)) (composer (alist-get 'composer current)) (no (string-to-number (alist-get 'songpos current "0"))) (tims (concat " [" (jao-mpc--current-timestr times 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 (or title "") 'jao-themes-f00) (jao--put-face (or 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) (jao--put-face tims (if times 'jao-themes-f00 '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 jao-random-album-p (not (jao-mpc--current-str port))) (let ((inhibit-message t)) (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))))) (defvar jao-mpc--browser-port nil) (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" jao-mpc--browser-port)) (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 port idp) (interactive) (let ((a (or album (string-trim (thing-at-point 'line)))) (p (or port jao-mpc--browser-port))) (jao-mpc--cmd "clear" p) (jao-mpc--cmd (if idp (concat "add " a) (format "findadd album \"%s\"" a)) p) (jao-mpc--cmd "play" p))) (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) (message "%s" (jao-mpc--current-str port t))) ;;;###autoload (defun jao-mpc-add-url (url) (interactive "sURL: ") (jao-mpc--cmd (format "add %s" url))) ;;;###autoload (defun jao-mpc-add-or-play-url (url &optional play) "Add the given URL to mpc's playing list, or just play it." (let ((p (or play (yes-or-no-p (format "Play %s right now?" url))))) (when p (jao-mpc-clear)) (jao-mpc-add-url url) (if p (jao-mpc-play) (message "%s added to mpc queue" url)))) (defvar jao-mpc-stream-urls '(("classic fm" . "http://media-ice.musicradio.com:80/ClassicFMMP3") ("wcpe" . "http://audio-mp3.ibiblio.org:8000/wcpe.mp3") ("davide of mimic" . "http://streaming01.zfast.co.uk:8018/stream") ("cinemix" . "http://94.23.51.96:8000") ;; 209.9.238.4:6022 209.9.238.4:6046 ("bbc gold" . "http://media-ice.musicradio.com:80/GoldMP3") ("irish gold" . "http://icecast2.rte.ie/gold"))) ;;;###autoload (defun jao-mpc-play-stream () "Select a predefined stream URL and add or play it in mpc." (interactive) (let ((s (completing-read "Stream: " jao-mpc-stream-urls))) (jao-mpc-add-or-play-url (cdr (assoc s jao-mpc-stream-urls)) t))) ;;;###autoload (defun jao-mpc-show-albums (&optional port) "Show album list." (interactive) (setq jao-mpc--browser-port port) (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))))) (defconst jao-mpc--albums-cmd "-f '%album% - %artist%' find \"(ALBUM =~ '.*')\" | uniq") (defconst jao-mpc--simple-albums-cmd "list album") ;;;###autoload (defun jao-mpc-select-album (&optional port) (interactive) (let* ((albums-str (jao-mpc--cmd jao-mpc--albums-cmd port)) (albums-str (if (string= "" albums-str) (jao-mpc--cmd jao-mpc--simple-albums-cmd port) albums-str)) (albums (split-string albums-str "\n" t))) (when-let (album (completing-read "Play album: " albums nil t)) (jao-mpc--add-and-play (car (split-string album "-" t " ")) port)))) (defconst jao-mpc--search-cmd "-f '%%album%% - %%artist%% :::%%file%%' search %s '%s'|grep :::tidal:album") (defun jao-mpc--search-albums (query) (let* ((cmd (format jao-mpc--search-cmd "any" query)) (str (jao-mpc--cmd cmd)) (res (split-string str "\n" t))) (mapcar (lambda (s) (split-string s ":::" t " ")) res))) ;;;###autoload (defun jao-mpc-search-and-select-album (&optional query port) (interactive "sSearch terms: ") (let* ((jao-mpc-port (or port jao-mpc-port)) (resa (jao-mpc--search-albums query))) (if (null resa) (user-error "No results") (when-let* ((a (completing-read "Play album: " resa nil t)) (s (car (alist-get a resa nil nil 'string=)))) (jao-mpc--add-and-play s port t))))) (provide 'jao-mpc) ;;; jao-mpc.el ends here