From e102891fb3bbb3fec134b5c678a0dd2306b9beaf Mon Sep 17 00:00:00 2001 From: Yoni Rabkin Date: Wed, 3 Jun 2020 11:52:04 -0400 Subject: move all files to top-level --- emms-player-mpd.el | 1320 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1320 insertions(+) create mode 100644 emms-player-mpd.el (limited to 'emms-player-mpd.el') diff --git a/emms-player-mpd.el b/emms-player-mpd.el new file mode 100644 index 0000000..327938d --- /dev/null +++ b/emms-player-mpd.el @@ -0,0 +1,1320 @@ +;;; emms-player-mpd.el --- MusicPD support for EMMS + +;; Copyright (C) 2005, 2006, 2007, 2008, 2009, 2014 Free Software Foundation, Inc. + +;; Author: Michael Olson , Jose Antonio Ortega Ruiz +;; + +;; This file is part of EMMS. + +;; EMMS 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, or (at your option) +;; any later version. +;; +;; EMMS 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 EMMS; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +;; Boston, MA 02110-1301, USA. + +;;; Commentary: + +;;; Benefits of MusicPD + +;; MusicPD features crossfade, very little skipping, minor CPU usage, +;; many clients, many supported output formats, fast manipulation via +;; network processes, and good abstraction of client and server. + +;;; MusicPD setup + +;; If you want to set up a local MusicPD server, you'll need to have +;; mpd installed. If you want to use a remote server instance, no +;; installation is needed. + +;; The website is at http://musicpd.org/. Debian packages are +;; available. It is recommended to use mpd version 0.12.0 or higher. +;; +;; Copy the example configuration for mpd into ~/.mpdconf and edit it +;; to your needs. Use your top level music directory for +;; music_directory. If your playlists use absolute file names, be +;; certain that music_directory has the leading directory part. +;; +;; Before you try to play anything, but after setting up the above, +;; run `mkdir ~/.mpd && mpd --create-db' to create MusicPD's track +;; database. +;; +;; Check to see if mpd is running. It must be running as a daemon for +;; you to be able to play anything. Launch it by executing "mpd". It +;; can be killed later with "mpd --kill" (or just "killall mpd" if +;; you're not using the latest development version). + +;;; EMMS setup + +;; Add the following to your config. +;; +;; (require 'emms-player-mpd) + +;; Adjust `emms-player-mpd-server-name' and +;; `emms-player-mpd-server-port' to match the location and port of +;; your MusicPD server. +;; +;; (setq emms-player-mpd-server-name "localhost") +;; (setq emms-player-mpd-server-port "6600") + +;; If your MusicPD setup requires a password, you will need to do the +;; following. +;; +;; (setq emms-player-mpd-server-password "mypassword") + +;; To get track info from MusicPD, do the following. +;; +;; (add-to-list 'emms-info-functions 'emms-info-mpd) + +;; To change the volume using MusicPD, do the following. +;; +;; (setq emms-volume-change-function 'emms-volume-mpd-change) + +;; Add 'emms-player-mpd to the top of `emms-player-list'. +;; +;; (add-to-list 'emms-player-list 'emms-player-mpd) + +;; If you use absolute file names in your m3u playlists (which is most +;; likely), make sure you set `emms-player-mpd-music-directory' to the +;; value of "music_directory" from your MusicPD config. There are +;; additional options available as well, but the defaults should be +;; sufficient for most uses. + +;; You can set `emms-player-mpd-sync-playlist' to nil if your master +;; EMMS playlist contains only stored playlists. + +;; If at any time you wish to replace the current EMMS playlist buffer +;; with the contents of the MusicPD playlist, type +;; M-x emms-player-mpd-connect. +;; +;; This will also run the relevant seek functions, so that if you use +;; emms-playing-time, the displayed time will be accurate. + +;;; Contributors + +;; Adam Sjøgren implemented support for changing the volume. + +(require 'cl-lib) +(require 'emms-player-simple) +(require 'emms-source-playlist) ; for emms-source-file-parse-playlist +(require 'tq) +(require 'emms-cache) +(require 'emms-url) + +(eval-when-compile + (condition-case nil + (progn + (require 'url) ; load if available + (require 'emms-url)) + (error nil))) + +(defgroup emms-player-mpd nil + "EMMS player for MusicPD." + :group 'emms-player + :prefix "emms-player-mpd-") + +(defcustom emms-player-mpd (emms-player 'emms-player-mpd-start + 'emms-player-mpd-stop + 'emms-player-mpd-playable-p) + "*Parameters for the MusicPD player." + :type '(cons symbol alist) + :group 'emms-player-mpd) + +(defcustom emms-player-mpd-music-directory nil + "The value of 'music_directory' in your MusicPD configuration file. + +Unless your MusicPD is configured to use absolute file names, you must +set this variable to the value of 'music_directory' in your MusicPD +config." + ;; The :format part ensures that entering directories happens on the + ;; next line, where there is more space to work with + :type '(choice :format "%{%t%}:\n %[Value Menu%] %v" + (const nil) + directory) + :group 'emms-player-mpd) + +(defun emms-player-mpd-get-supported-regexp () + "Returns a regexp of file extensions that MusicPD supports, +or nil if we cannot figure it out." + (let ((out (shell-command-to-string "mpd --version"))) + ;; 0.17.x + (if (string-match "Decoders plugins:$" out) + (let* ((b (match-end 0)) + (e (string-match "Output plugins:$" out)) + (plugs (split-string (substring out b e) "\n" t)) + (plugs (cl-mapcan (lambda (x) + (and (string-match " +\\[.*\\] +\\(.+\\)$" x) + (split-string (match-string 1 x) nil t))) + plugs)) + (b (and (string-match "Protocols:$" out) (match-end 0))) + (prots (and b (substring out (+ 2 b) -1))) + (prots (split-string (or prots "") nil t))) + (concat "\\(\\.\\(m3u\\|pls\\|" + (regexp-opt (delq nil plugs)) + "\\)\\'\\)\\|\\(\\`" + (regexp-opt (delete "file://" prots)) "\\)")) + (let ((found-start nil) + (supported nil)) + (if (string-match "Supported decoders:\\([^0]+?\\)Supported outputs:" out) + ;; 0.15.x + (setq supported (replace-regexp-in-string "\\[.+?\\]" "" + (match-string 1 out))) + ;; < 0.15 + (setq out (split-string out "\n")) + (while (car out) + (cond ((string= (car out) "Supported formats:") + (setq found-start t)) + ((string= (car out) "") + (setq found-start nil)) + (found-start + (setq supported (concat supported (car out))))) + (setq out (cdr out)))) + ;; Create regexp + (when (and (stringp supported) + (not (string= supported ""))) + (concat "\\`http://\\|\\.\\(m3u\\|pls\\|" + (regexp-opt (delq nil (split-string supported))) + "\\)\\'")))))) + +(defcustom emms-player-mpd-supported-regexp + ;; Use a sane default, just in case + (or (emms-player-mpd-get-supported-regexp) + (concat "\\`http://\\|" + (emms-player-simple-regexp + "m3u" "ogg" "flac" "mp3" "wav" "mod" "au" "aiff"))) + "Formats supported by MusicPD." + :type 'regexp + :set (function + (lambda (sym value) + (set sym value) + (emms-player-set emms-player-mpd 'regex value))) + :group 'emms-player-mpd) + +(defcustom emms-player-mpd-connect-function 'open-network-stream + "Function used to initiate the connection to MusicPD. +It should take same arguments as `open-network-stream' does." + :type 'function + :group 'emms-player-mpd) + +(defcustom emms-player-mpd-server-name (or (getenv "MPD_HOST") "localhost") + "The MusicPD server that we should connect to." + :type 'string + :group 'emms-player-mpd) + +(defcustom emms-player-mpd-server-port (or (getenv "MPD_PORT") "6600") + "The port of the MusicPD server that we should connect to." + :type '(choice number string) + :group 'emms-player-mpd) + +(defcustom emms-player-mpd-server-password nil + "The password for the MusicPD server that we should connect to." + :type '(choice (const :tag "None" nil) + string) + :group 'emms-player-mpd) + +(defcustom emms-player-mpd-check-interval 1 + "How often to check to see whether MusicPD has advanced to the +next song. This may be an integer, a floating point number, or +nil. If set to nil, this check will not be periodically +performed. + +This variable is used only if `emms-player-mpd-sync-playlist' is +non-nil." + :type '(choice (const :tag "Disable check" nil) + number) + :group 'emms-player-mpd) + +(defcustom emms-player-mpd-verbose nil + "Whether to provide notifications for server connection events +and errors." + :type 'boolean + :group 'emms-player-mpd) + +(defcustom emms-player-mpd-sync-playlist t + "Whether to synchronize the EMMS playlist with the MusicPD playlist. + +If your EMMS playlist contains music files rather than playlists, +leave this set to non-nil. + +If your EMMS playlist contains stored playlists, set this to nil." + :type 'boolean + :group 'emms-player-mpd) + +(emms-player-set emms-player-mpd + 'regex + emms-player-mpd-supported-regexp) + +(emms-player-set emms-player-mpd + 'pause + 'emms-player-mpd-pause) + +(emms-player-set emms-player-mpd + 'resume + 'emms-player-mpd-pause) + +(emms-player-set emms-player-mpd + 'seek + 'emms-player-mpd-seek) + +(emms-player-set emms-player-mpd + 'seek-to + 'emms-player-mpd-seek-to) + +;;; Dealing with the MusicPD network process + +(defvar emms-player-mpd-process nil) +(defvar emms-player-mpd-queue nil) + +(defvar emms-player-mpd-playlist-id nil) +(make-variable-buffer-local 'emms-player-mpd-playlist-id) + +(defvar emms-player-mpd-current-song nil) +(defvar emms-player-mpd-last-state nil) +(defvar emms-player-mpd-status-timer nil) + +(defvar emms-player-mpd-status-regexp + "^\\(OK\\( MPD \\)?\\|ACK \\[\\([0-9]+\\)@[0-9]+\\] \\(.+\\)\\)\n+\\'" + "Regexp that matches the valid status strings that MusicPD can +return at the end of a request.") + +(defun emms-player-mpd-sentinel (proc event) + "The process sentinel for MusicPD." + (let ((status (process-status proc))) + (cond ((string-match "^deleted" event) + (when emms-player-mpd-verbose + (message "MusicPD process was deleted"))) + ((memq status '(exit signal closed)) + (emms-player-mpd-close-process t) + (when emms-player-mpd-verbose + (message "Closed MusicPD process"))) + ((memq status '(run open)) + (when emms-player-mpd-verbose + (message "MusicPD process started successfully"))) + (t + (when emms-player-mpd-verbose + (message "Other MusicPD status change: %s, %s" status event)))))) + +(defun emms-player-mpd-ensure-process () + "Make sure that a MusicPD process is currently active." + (unless (and emms-player-mpd-process + (processp emms-player-mpd-process) + (memq (process-status emms-player-mpd-process) '(run open))) + (setq emms-player-mpd-process + (if emms-player-mpd-server-port + (funcall emms-player-mpd-connect-function "mpd" + nil + emms-player-mpd-server-name + emms-player-mpd-server-port) + (make-network-process :name "emms-mpd" + :service emms-player-mpd-server-name + :family 'local))) + (set-process-sentinel emms-player-mpd-process + 'emms-player-mpd-sentinel) + (setq emms-player-mpd-queue + (tq-create emms-player-mpd-process)) + (if (fboundp 'set-process-query-on-exit-flag) + (set-process-query-on-exit-flag emms-player-mpd-process nil) + (set-process-query-on-exit-flag emms-player-mpd-process nil)) + ;; send password + (when (stringp emms-player-mpd-server-password) + (tq-enqueue emms-player-mpd-queue + (concat "password " emms-player-mpd-server-password "\n") + emms-player-mpd-status-regexp nil #'ignore t)))) + +(defun emms-player-mpd-close-process (&optional from-sentinel) + "Terminate the current MusicPD client process. +FROM-SENTINEL indicates whether this was called by the process sentinel, +in which case certain checks should not be made." + (when (or from-sentinel + (and (processp emms-player-mpd-process) + (memq (process-status emms-player-mpd-process) '(run open)))) + (tq-close emms-player-mpd-queue) + (setq emms-player-mpd-queue nil) + (setq emms-player-mpd-process nil))) + +(defun emms-player-mpd-send (question closure fn) + "Send the given QUESTION to the MusicPD server. +When a reply comes, call FN with CLOSURE and the result." + (emms-player-mpd-ensure-process) + (unless (string= (substring question -1) "\n") + (setq question (concat question "\n"))) + (tq-enqueue emms-player-mpd-queue question + emms-player-mpd-status-regexp + closure fn t)) + +;;; Helper functions + +(defun emms-player-mpd-get-mpd-filename (file) + "Turn FILE into something that MusicPD can understand. + +This usually means removing a prefix." + (if (or (not emms-player-mpd-music-directory) + (not (eq (aref file 0) ?/)) + (string-match "\\`http://" file)) + file + (file-relative-name file emms-player-mpd-music-directory))) + +(defun emms-player-mpd-get-emms-filename (file) + "Turn FILE into something that EMMS can understand. + +This usually means adding a prefix." + (if (or (not emms-player-mpd-music-directory) + (eq (aref file 0) ?/) + (string-match "\\`http://" file)) + file + (expand-file-name file emms-player-mpd-music-directory))) + +(defun emms-player-mpd-parse-response (response) + "Convert the given MusicPD response into a list. + +The car of the list is special: +If an error has occurred, it will contain a cons cell whose car is +an error number and whose cdr is the corresponding message. +Otherwise, it will be nil." + (when (stringp response) + (save-match-data + (let* ((data (split-string response "\n")) + (cruft (last data 3)) + (status (if (string= (cadr cruft) "") + (car cruft) + (cadr cruft)))) + (setcdr cruft nil) + (when (and (stringp (car data)) + (string-match "^OK\\( MPD \\)?" (car data))) + (setq data (cdr data))) + (if (and (stringp status) + (string-match "^ACK \\[\\([0-9]+\\)@[0-9]+\\] \\(.+\\)" + status)) + (cons (cons (match-string 1 status) + (match-string 2 status)) + data) + (cons nil data)))))) + +(defun emms-player-mpd-parse-line (line) + "Turn the given LINE from MusicPD into a cons cell. + +The format of the cell is (name . value)." + (when (string-match "\\`\\([^:\n]+\\):\\s-*\\(.+\\)" line) + (let ((name (match-string 1 line)) + (value (match-string 2 line))) + (if (and name value) + (progn + (setq name (downcase name)) + (cons name value)) + nil)))) + +(defun emms-player-mpd-get-alist (info) + "Turn the given parsed INFO from MusicPD into an alist." + (when (and info + (null (car info)) ; no error has occurred + (cdr info)) ; data exists + (let ((alist nil) + cell old-cell) + (dolist (line (cdr info)) + (when (setq cell (emms-player-mpd-parse-line line)) + (if (setq old-cell (assoc (car cell) alist)) + (setcdr old-cell (cdr cell)) + (setq alist (cons cell alist))))) + alist))) + +(defun emms-player-mpd-get-alists (info) + "Turn the given parsed INFO from MusicPD into an list of alists. + +The list will be in reverse order." + (when (and info + (null (car info)) ; no error has occurred + (cdr info)) ; data exists + (let ((alists nil) + (alist nil) + cell) + (dolist (line (cdr info)) + (when (setq cell (emms-player-mpd-parse-line line)) + (if (assoc (car cell) alist) + (setq alists (cons alist alists) + alist (list cell)) + (setq alist (cons cell alist))))) + (when alist + (setq alists (cons alist alists))) + alists))) + +(defun emms-player-mpd-get-tracks-1 (closure response) + (let ((songs (emms-player-mpd-get-alists + (emms-player-mpd-parse-response response))) + (tracks nil)) + (when songs + (dolist (song-info songs) + (let ((file (cdr (assoc "file" song-info)))) + (when file + (setq file (emms-player-mpd-get-emms-filename file)) + (let* ((type (if (string-match "\\`http://" file) + 'url + 'file)) + (track (emms-track type file))) + (emms-info-mpd track song-info) + (run-hook-with-args 'emms-track-info-filters track) + (setq tracks (cons track tracks))))))) + (funcall (car closure) (cdr closure) tracks))) + +(defun emms-player-mpd-get-tracks (closure callback) + "Get the current playlist from MusicPD in the form of a list of +EMMS tracks. +Call CALLBACK with CLOSURE and result when the request is complete." + (emms-player-mpd-send "playlistinfo" (cons callback closure) + #'emms-player-mpd-get-tracks-1)) + +(defun emms-player-mpd-get-status-1 (closure response) + (funcall (car closure) + (cdr closure) + (emms-player-mpd-get-alist + (emms-player-mpd-parse-response response)))) + +(defun emms-player-mpd-get-status (closure callback) + "Get status information from MusicPD. +It will be returned in the form of an alist by calling CALLBACK +with CLOSURE as its first argument, and the status as the +second." + (emms-player-mpd-send "status" (cons callback closure) + #'emms-player-mpd-get-status-1)) + +(defun emms-player-mpd-get-status-part (closure callback item &optional info) + "Get ITEM from the current MusicPD status. +Call CALLBACK with CLOSURE and result when the request is complete. +If INFO is specified, use that instead of acquiring the necessary +info from MusicPD." + (if info + (funcall callback closure (cdr (assoc item info))) + (emms-player-mpd-get-status + (cons callback (cons closure item)) + (lambda (closure info) + (let ((fn (car closure)) + (close (cadr closure)) + (item (cddr closure))) + (funcall fn close (cdr (assoc item info)))))))) + +(defun emms-player-mpd-get-playlist-id (closure callback &optional info) + "Get the current playlist ID from MusicPD. +Call CALLBACK with CLOSURE and result when the request is complete. +If INFO is specified, use that instead of acquiring the necessary +info from MusicPD." + (when info + (setq callback (lambda (closure id) id))) + (emms-player-mpd-get-status-part closure callback "playlist" info)) + +(defun emms-player-mpd-get-volume (closure callback &optional info) + "Get the current volume from MusicPD. +Call CALLBACK with CLOSURE and result when the request is complete. +If INFO is specified, use that instead of acquiring the necessary +info from MusicPD." + (when info + (setq callback (lambda (closure volume) volume))) + (emms-player-mpd-get-status-part closure callback "volume" info)) + +(defun emms-player-mpd-get-current-song (closure callback &optional info) + "Get the current song from MusicPD. +This is in the form of a number that indicates the position of +the song on the current playlist. + +Call CALLBACK with CLOSURE and result when the request is complete. +If INFO is specified, use that instead of acquiring the necessary +info from MusicPD." + (when info + (setq callback (lambda (closure id) id))) + (emms-player-mpd-get-status-part closure callback "song" info)) + +(defun emms-player-mpd-get-mpd-state (closure callback &optional info) + "Get the current state of the MusicPD server. +This is either \"play\", \"stop\", or \"pause\". + +Call CALLBACK with CLOSURE and result when the request is complete. +If INFO is specified, use that instead of acquiring the necessary +info from MusicPD." + (when info + (setq callback (lambda (closure id) id))) + (emms-player-mpd-get-status-part closure callback "state" info)) + +(defun emms-player-mpd-get-playing-time (closure callback &optional info) + "Get the number of seconds that the current song has been playing, +or nil if we cannot obtain this information. + +Call CALLBACK with CLOSURE and result when the request is complete. +If INFO is specified, use that instead of acquiring the necessary +info from MusicPD." + (if info + (emms-player-mpd-get-status-part + nil + (lambda (closure time) + (and time + (string-match "\\`\\([0-9]+\\):" time) + (string-to-number (match-string 1 time)))) + "time" info) + (emms-player-mpd-get-status-part + (cons callback closure) + (lambda (closure time) + (funcall (car closure) + (cdr closure) + (and time + (string-match "\\`\\([0-9]+\\):" time) + (string-to-number (match-string 1 time))))) + "time" info))) + +(defun emms-player-mpd-select-song (prev-song new-song) + "Move to the given song position. + +The amount to move is the number difference between PREV-SONG and +NEW-SONG. NEW-SONG should be a string containing a number. +PREV-SONG may be either a string containing a number or nil, +which indicates that we should start from the beginning of the +buffer and move to NEW-SONG." + (with-current-emms-playlist + ;; move to current track + (goto-char (if (and (stringp prev-song) + emms-playlist-selected-marker + (marker-position emms-playlist-selected-marker)) + emms-playlist-selected-marker + (point-min))) + ;; seek forward or backward + (let ((diff (if (stringp prev-song) + (- (string-to-number new-song) + (string-to-number prev-song)) + (string-to-number new-song)))) + (condition-case nil + (progn + ;; skip to first track if not on one + (when (and (> diff 0) + (not (emms-playlist-track-at (point)))) + (emms-playlist-next)) + ;; move to new track + (while (> diff 0) + (emms-playlist-next) + (setq diff (- diff 1))) + (while (< diff 0) + (emms-playlist-previous) + (setq diff (+ diff 1))) + ;; select track at point + (unless (emms-playlist-selected-track-at-p) + (emms-playlist-select (point)))) + (error (concat "Could not move to position " new-song)))))) + +(defun emms-player-mpd-sync-from-emms-1 (closure) + (emms-player-mpd-get-playlist-id + closure + (lambda (closure id) + (let ((buffer (car closure)) + (fn (cdr closure))) + (when (functionp fn) + (funcall fn buffer id)))))) + +(defun emms-player-mpd-sync-from-emms (&optional callback) + "Synchronize the MusicPD playlist with the contents of the +current EMMS playlist. + +If CALLBACK is provided, call it with the current EMMS playlist +buffer and MusicPD playlist ID when we are done, if there were no +errors." + (emms-player-mpd-clear) + (with-current-emms-playlist + (let (tracks) + (save-excursion + (setq tracks (nreverse + (emms-playlist-tracks-in-region + (point-min) (point-max))))) + (emms-player-mpd-add-several-tracks + tracks + (cons (current-buffer) callback) + #'emms-player-mpd-sync-from-emms-1)))) + +(defun emms-player-mpd-sync-from-mpd-2 (closure info) + (let ((buffer (car closure)) + (fn (cadr closure)) + (close (cddr closure)) + (id (emms-player-mpd-get-playlist-id nil #'ignore info)) + (song (emms-player-mpd-get-current-song nil #'ignore info))) + (when (buffer-live-p buffer) + (let ((emms-playlist-buffer buffer)) + (with-current-emms-playlist + (setq emms-player-mpd-playlist-id id) + (set-buffer-modified-p nil) + (if song + (emms-player-mpd-select-song nil song) + (goto-char (point-min))))) + (when (functionp fn) + (funcall fn close info))))) + +(defun emms-player-mpd-sync-from-mpd-1 (closure tracks) + (let ((buffer (car closure))) + (when (and tracks + (buffer-live-p buffer)) + (let ((emms-playlist-buffer buffer)) + (with-current-emms-playlist + (emms-playlist-clear) + (mapc #'emms-playlist-insert-track tracks))) + (emms-player-mpd-get-status closure + #'emms-player-mpd-sync-from-mpd-2)))) + +(defun emms-player-mpd-sync-from-mpd (&optional closure callback) + "Synchronize the EMMS playlist with the contents of the current +MusicPD playlist. Namely, clear the EMMS playlist buffer and add +tracks to it that are present in the MusicPD playlist. + +If the current buffer is an EMMS playlist buffer, make it the +main EMMS playlist buffer." + (when (and emms-playlist-buffer-p + (not (eq (current-buffer) emms-playlist-buffer))) + (emms-playlist-set-playlist-buffer (current-buffer))) + (with-current-emms-playlist + (emms-player-mpd-get-tracks + (cons emms-playlist-buffer (cons callback closure)) + #'emms-player-mpd-sync-from-mpd-1))) + +(defun emms-player-mpd-detect-song-change-2 (state info) + "Perform post-sync tasks after returning from a stop." + (setq emms-player-mpd-current-song nil) + (setq emms-player-playing-p 'emms-player-mpd) + (when (string= state "pause") + (setq emms-player-paused-p t)) + (emms-player-mpd-detect-song-change info)) + +(defun emms-player-mpd-detect-song-change-1 (closure info) + (let ((song (emms-player-mpd-get-current-song nil #'ignore info)) + (state (emms-player-mpd-get-mpd-state nil #'ignore info)) + (time (emms-player-mpd-get-playing-time nil #'ignore info)) + (err-msg (cdr (assoc "error" info)))) + (if (stringp err-msg) + (progn + (message "MusicPD error: %s" err-msg) + (emms-player-mpd-send + "clearerror" + nil #'ignore)) + (cond ((string= state "stop") + (if song + ;; a track remains: the user probably stopped MusicPD + ;; manually, so we'll stop EMMS completely + (let ((emms-player-stopped-p t)) + (setq emms-player-mpd-last-state "stop") + (emms-player-stopped)) + ;; no more tracks are left: we probably ran out of things + ;; to play, so let EMMS do something further if it wants + (unless (string= emms-player-mpd-last-state "stop") + (setq emms-player-mpd-last-state "stop") + (emms-player-stopped)))) + ((and emms-player-mpd-last-state + (string= emms-player-mpd-last-state "stop")) + ;; resume from a stop that occurred outside of EMMS + (setq emms-player-mpd-last-state nil) + (emms-player-mpd-sync-from-mpd + state + #'emms-player-mpd-detect-song-change-2)) + ((string= state "pause") + nil) + ((string= state "play") + (setq emms-player-mpd-last-state "play") + (unless (or (null song) + (and (stringp emms-player-mpd-current-song) + (string= song emms-player-mpd-current-song))) + (let ((emms-player-stopped-p t)) + (emms-player-stopped)) + (emms-player-mpd-select-song emms-player-mpd-current-song song) + (setq emms-player-mpd-current-song song) + (emms-player-started 'emms-player-mpd) + (when time + (run-hook-with-args 'emms-player-time-set-functions + time)))))))) + +(defun emms-player-mpd-detect-song-change (&optional info) + "Detect whether a song change has occurred. +This is usually called by a timer. + +If INFO is specified, use that instead of acquiring the necessary +info from MusicPD." + (if info + (emms-player-mpd-detect-song-change-1 nil info) + (emms-player-mpd-get-status nil #'emms-player-mpd-detect-song-change-1))) + +(defun emms-player-mpd-quote-file (file) + "Escape special characters in FILE and surround in double-quotes." + (concat "\"" + (emms-replace-regexp-in-string + "\"" "\\\\\"" + (emms-replace-regexp-in-string "\\\\" "\\\\\\\\" file)) + "\"")) + +;;;###autoload +(defun emms-player-mpd-clear () + "Clear the MusicPD playlist." + (interactive) + (when emms-player-mpd-status-timer + (emms-cancel-timer emms-player-mpd-status-timer) + (setq emms-player-mpd-status-timer nil)) + (setq emms-player-mpd-last-state nil) + (emms-player-mpd-send "clear" nil #'ignore) + (let ((emms-player-stopped-p t)) + (emms-player-stopped))) + +;;; Adding to the MusicPD playlist + +(defun emms-player-mpd-add-file (file closure callback) + "Add FILE to the current MusicPD playlist. +Execute CALLBACK with CLOSURE as its first argument when done. + +If an error occurs, display a relevant message." + (setq file (emms-player-mpd-get-mpd-filename file)) + (emms-player-mpd-send + (concat "add " (emms-player-mpd-quote-file file)) + (cons file (cons callback closure)) + (lambda (closure response) + (let ((output (emms-player-mpd-parse-response response)) + (file (car closure)) + (callback (cadr closure)) + (close (cddr closure))) + (if (car output) + (message "MusicPD error: %s: %s" file (cdar output)) + (when (functionp callback) + (funcall callback close))))))) + +(defun emms-player-mpd-add-buffer-contents (buffer closure callback) + "Load contents of BUFFER into MusicPD by adding each line. +Execute CALLBACK with CLOSURE as its first argument when done. + +This handles both m3u and pls type playlists." + (with-current-buffer buffer + (goto-char (point-min)) + (let ((format (emms-source-playlist-determine-format))) + (when format + (emms-player-mpd-add-several-files + (emms-source-playlist-files format) + closure callback))))) + +(defun emms-player-mpd-add-playlist (playlist closure callback) + "Load contents of PLAYLIST into MusicPD by adding each line. +Execute CALLBACK with CLOSURE as its first argument when done. + +This handles both m3u and pls type playlists." + ;; This is useful for playlists of playlists + (with-temp-buffer + (emms-insert-file-contents playlist) + (emms-player-mpd-add-buffer-contents (current-buffer) closure callback))) + +(defun emms-player-mpd-add-streamlist (url closure callback) + "Download contents of URL and then add its feeds into MusicPD. +Execute CALLBACK with CLOSURE as its first argument when done." + ;; This is useful with emms-streams.el + (if (fboundp 'url-insert-file-contents) + (progn + (require 'emms-url) + (with-temp-buffer + (url-insert-file-contents (emms-url-quote-entire url)) + (emms-http-decode-buffer (current-buffer)) + (emms-player-mpd-add-buffer-contents (current-buffer) + closure callback))) + (error (message (concat "You need to install url.el so that" + " Emms can retrieve this stream"))))) + +(defun emms-player-mpd-add (track closure callback) + "Add TRACK to the MusicPD playlist. +Execute CALLBACK with CLOSURE as its first argument when done." + (let ((name (emms-track-get track 'name)) + (type (emms-track-get track 'type))) + (cond ((eq type 'url) + (emms-player-mpd-add-file name closure callback)) + ((eq type 'streamlist) + (emms-player-mpd-add-streamlist name closure callback)) + ((or (eq type 'playlist) + (string-match "\\.\\(m3u\\|pls\\)\\'" name)) + (emms-player-mpd-add-playlist name closure callback)) + ((and (eq type 'file) + (string-match emms-player-mpd-supported-regexp name)) + (emms-player-mpd-add-file name closure callback))))) + +(defun emms-player-mpd-add-several-tracks (tracks closure callback) + "Add TRACKS to the MusicPD playlist. +Execute CALLBACK with CLOSURE as its first argument when done." + (when (consp tracks) + (while (cdr tracks) + (emms-player-mpd-add (car tracks) nil #'ignore) + (setq tracks (cdr tracks))) + ;; only execute callback on last track + (emms-player-mpd-add (car tracks) closure callback))) + +(defun emms-player-mpd-add-several-files (files closure callback) + "Add FILES to the MusicPD playlist. +Execute CALLBACK with CLOSURE as its first argument when done." + (when (consp files) + (while (cdr files) + (emms-player-mpd-add-file (car files) nil #'ignore) + (setq files (cdr files))) + ;; only execute callback on last file + (emms-player-mpd-add-file (car files) closure callback))) + +;;; EMMS API + +(defun emms-player-mpd-playable-p (track) + "Return non-nil when we can play this track." + (and (memq (emms-track-type track) '(file url playlist streamlist)) + (string-match (emms-player-get emms-player-mpd 'regex) + (emms-track-name track)) + (condition-case nil + (progn (emms-player-mpd-ensure-process) + t) + (error nil)))) + +(defun emms-player-mpd-play (&optional id) + "Play whatever is in the current MusicPD playlist. +If ID is specified, play the song at that position in the MusicPD +playlist." + (interactive) + (if id + (progn + (unless (stringp id) + (setq id (number-to-string id))) + (emms-player-mpd-send + (concat "play " id) + nil + (lambda (closure response) + (setq emms-player-mpd-current-song nil) + (if emms-player-mpd-check-interval + (setq emms-player-mpd-status-timer + (run-at-time t emms-player-mpd-check-interval + 'emms-player-mpd-detect-song-change)) + (emms-player-mpd-detect-song-change))))) + ;; we only want to play one track, so don't start the timer + (emms-player-mpd-send + "play" + nil + (lambda (closure response) + (emms-player-started 'emms-player-mpd))))) + +(defun emms-player-mpd-start-and-sync-2 (buffer id) + (when (buffer-live-p buffer) + (let ((emms-playlist-buffer buffer)) + (with-current-emms-playlist + (setq emms-player-mpd-playlist-id id) + (set-buffer-modified-p nil) + (let ((track-cnt 0)) + (save-excursion + (goto-char + (if (and emms-playlist-selected-marker + (marker-position emms-playlist-selected-marker)) + emms-playlist-selected-marker + (point-min))) + (condition-case nil + (while t + (emms-playlist-previous) + (setq track-cnt (1+ track-cnt))) + (error nil))) + (emms-player-mpd-play track-cnt)))))) + +(defun emms-player-mpd-start-and-sync-1 (closure id) + (let ((buf-id (with-current-emms-playlist + emms-player-mpd-playlist-id))) + (if (and (not (buffer-modified-p emms-playlist-buffer)) + (stringp buf-id) + (string= buf-id id)) + (emms-player-mpd-start-and-sync-2 emms-playlist-buffer id) + (emms-player-mpd-sync-from-emms + #'emms-player-mpd-start-and-sync-2)))) + +(defun emms-player-mpd-start-and-sync () + "Ensure that MusicPD's playlist is up-to-date with EMMS's +playlist, and then play the current track. + +This is called if `emms-player-mpd-sync-playlist' is non-nil." + (when emms-player-mpd-status-timer + (emms-cancel-timer emms-player-mpd-status-timer) + (setq emms-player-mpd-status-timer nil)) + (emms-player-mpd-send + "clearerror" + nil + (lambda (closure response) + (emms-player-mpd-get-playlist-id + nil + #'emms-player-mpd-start-and-sync-1)))) + +(defun emms-player-mpd-connect-1 (closure info) + (setq emms-player-mpd-current-song nil) + (let* ((state (emms-player-mpd-get-mpd-state nil #'ignore info))) + (unless (string= state "stop") + (setq emms-player-playing-p 'emms-player-mpd)) + (when (string= state "pause") + (setq emms-player-paused-p t)) + (unless (string= state "stop") + (emms-player-mpd-detect-song-change info) + (when emms-player-mpd-check-interval + (setq emms-player-mpd-status-timer + (run-at-time t emms-player-mpd-check-interval + 'emms-player-mpd-detect-song-change)))))) + +;;;###autoload +(defun emms-player-mpd-connect () + "Connect to MusicPD and retrieve its current playlist. + +Afterward, the status of MusicPD will be tracked. + +This also has the effect of changing the current EMMS playlist to +be the same as the current MusicPD playlist. Thus, this +function is useful to call if the contents of the EMMS playlist +buffer get out-of-sync for some reason." + (interactive) + (when emms-player-mpd-status-timer + (emms-cancel-timer emms-player-mpd-status-timer) + (setq emms-player-mpd-status-timer nil)) + (emms-player-mpd-sync-from-mpd + nil #'emms-player-mpd-connect-1)) + +(defun emms-player-mpd-start (track) + "Starts a process playing TRACK." + (interactive) + (if (and emms-player-mpd-sync-playlist + (not (memq (emms-track-get track 'type) '(streamlist playlist)))) + (emms-player-mpd-start-and-sync) + (emms-player-mpd-clear) + ;; if we have loaded the item successfully, play it + (emms-player-mpd-add track nil #'emms-player-mpd-play))) + +(defun emms-player-mpd-disconnect (&optional no-stop) + "Terminate the MusicPD client process and disconnect from MusicPD. + +If NO-STOP is non-nil, do not indicate to EMMS that we are +stopped. This argument is meant to be used when calling this +from other functions." + (interactive) + (emms-cancel-timer emms-player-mpd-status-timer) + (setq emms-player-mpd-status-timer nil) + (setq emms-player-mpd-current-song nil) + (setq emms-player-mpd-last-state nil) + (emms-player-mpd-close-process) + (unless no-stop + (let ((emms-player-stopped-p t)) + (emms-player-stopped)))) + +(defun emms-player-mpd-stop () + "Stop the currently playing song." + (interactive) + (condition-case nil + (emms-player-mpd-send "stop" nil #'ignore) + (error nil)) + (emms-player-mpd-disconnect t) + (let ((emms-player-stopped-p t)) + (emms-player-stopped))) + +(defun emms-player-mpd-pause () + "Pause the currently playing song." + (interactive) + (emms-player-mpd-send "pause" nil #'ignore)) + +(defun emms-player-mpd-seek (amount) + "Seek backward or forward by AMOUNT seconds, depending on sign of AMOUNT." + (interactive) + (emms-player-mpd-get-status + amount + (lambda (amount info) + (let ((song (emms-player-mpd-get-current-song nil #'ignore info)) + (secs (emms-player-mpd-get-playing-time nil #'ignore info))) + (when (and song secs) + (emms-player-mpd-send + (concat "seek " song " " (number-to-string (round (+ secs amount)))) + nil #'ignore)))))) + +(defun emms-player-mpd-seek-to (pos) + "Seek to POS seconds from the start of the current track." + (interactive) + (emms-player-mpd-get-current-song + pos + (lambda (pos song) + (when (and song pos) + (emms-player-mpd-send + (concat "seek " song " " (number-to-string (round pos))) + nil #'ignore))))) + +(defun emms-player-mpd-next () + "Move forward by one track in MusicPD's internal playlist." + (interactive) + (emms-player-mpd-send "next" nil #'ignore)) + +(defun emms-player-mpd-previous () + "Move backward by one track in MusicPD's internal playlist." + (interactive) + (emms-player-mpd-send "previous" nil #'ignore)) + +;;; Volume + +(defun emms-volume-mpd-change (amount) + "Change volume up or down by AMOUNT, depending on whether it is +positive or negative." + (interactive "MVolume change amount (+ increase, - decrease): ") + (emms-player-mpd-get-volume + amount + (lambda (change volume) + (let ((new-volume (+ (string-to-number volume) change))) + (emms-player-mpd-send + (concat "setvol \"" (number-to-string new-volume) "\"") + nil #'ignore))))) + +;;; Now playing + +(defun emms-player-mpd-show-1 (closure response) + (let* ((info (emms-player-mpd-get-alist + (emms-player-mpd-parse-response response))) + (insertp (car closure)) + (callback (cadr closure)) + (buffer (cddr closure)) + (name (cdr (assoc "name" info))) ; radio feeds sometimes set this + (file (cdr (assoc "file" info))) + (desc nil)) + ;; if we are playing lastfm radio, use its show function instead + (if (and (boundp 'emms-lastfm-radio-stream-url) + (stringp emms-lastfm-radio-stream-url) + (string= emms-lastfm-radio-stream-url file)) + (with-current-buffer buffer + (and (fboundp 'emms-lastfm-np) + (emms-lastfm-np insertp callback))) + ;; otherwise build and show the description + (when info + (when name + (setq desc name)) + (when file + (let ((track (emms-dictionary '*track*)) + track-desc) + (if (string-match "\\`http://" file) + (emms-track-set track 'type 'url) + (emms-track-set track 'type 'file)) + (emms-track-set track 'name file) + (emms-info-mpd track info) + (run-hook-with-args 'emms-track-info-filters track) + (setq track-desc (emms-track-description track)) + (when (and (stringp track-desc) (not (string= track-desc ""))) + (setq desc (if desc + (concat desc ": " track-desc) + track-desc)))))) + (if (not desc) + (unless (functionp callback) + (message "Nothing playing right now")) + (setq desc (format emms-show-format desc)) + (cond ((functionp callback) + (funcall callback buffer desc)) + (insertp + (when (buffer-live-p buffer) + (with-current-buffer buffer + (insert desc)))) + (t + (message "%s" desc))))))) + +;;;###autoload +(defun emms-player-mpd-show (&optional insertp callback) + "Describe the current EMMS track in the minibuffer. + +If INSERTP is non-nil, insert the description into the current +buffer instead. + +If CALLBACK is a function, call it with the current buffer and +description as arguments instead of displaying the description or +inserting it. + +This function uses `emms-show-format' to format the current track. +It differs from `emms-show' in that it asks MusicPD for the current track, +rather than EMMS." + (interactive "P") + (emms-player-mpd-send "currentsong" + (cons insertp (cons callback (current-buffer))) + #'emms-player-mpd-show-1)) + +;;; Track info + +(defun emms-info-mpd-process (track info) + (dolist (data info) + (let ((name (car data)) + (value (cdr data))) + (setq name (cond ((string= name "artist") 'info-artist) + ((string= name "composer") 'info-composer) + ((string= name "performer") 'info-performer) + ((string= name "title") 'info-title) + ((string= name "album") 'info-album) + ((string= name "track") 'info-tracknumber) + ((string= name "disc") 'info-discnumber) + ((string= name "date") 'info-year) + ((string= name "genre") 'info-genre) + ((string= name "time") + (setq value (string-to-number value)) + 'info-playing-time) + (t nil))) + (when name + (emms-track-set track name value))))) + +(defun emms-info-mpd-1 (track response) + (let ((info (emms-player-mpd-get-alist + (emms-player-mpd-parse-response response)))) + (when info + (emms-info-mpd-process track info) + (emms-track-updated track)))) + +(defun emms-info-mpd (track &optional info) + "Add track information to TRACK. +If INFO is specified, use that instead of acquiring the necessary +info from MusicPD. + +This is a useful addition to `emms-info-functions'." + (if info + (emms-info-mpd-process track info) + (when (and (eq 'file (emms-track-type track)) + (not (string-match "\\`http://" (emms-track-name track)))) + (let ((file (emms-player-mpd-get-mpd-filename (emms-track-name track)))) + (when (or emms-player-mpd-music-directory + (and file + (string-match emms-player-mpd-supported-regexp file))) + (condition-case nil + (emms-player-mpd-send + (concat "find filename " + (emms-player-mpd-quote-file file)) + track + #'emms-info-mpd-1) + (error nil))))))) + +;;; Caching + +(defun emms-cache-set-from-mpd-track (track-info) + "Dump TRACK-INFO into the EMMS cache. + +The track should be an alist as per `emms-player-mpd-get-alist'." + (when emms-cache-set-function + (let ((track (emms-dictionary '*track*)) + (name (cdr (assoc "file" track-info)))) + (when name + (setq name (emms-player-mpd-get-emms-filename name)) + (emms-track-set track 'type 'file) + (emms-track-set track 'name name) + (emms-info-mpd-process track track-info) + (funcall emms-cache-set-function 'file name track))))) + +(defun emms-cache--info-cleanup (info) + (let ((xs (mapcar (lambda (x) + (and (stringp x) + (not (string-match-p "\\`\\(Last-\\|direct\\)" x)) + x)) + info))) + (cons nil (delq nil xs)))) + +(defun emms-cache-set-from-mpd-directory (dir) + "Dump all MusicPD data from DIR into the EMMS cache. + +This is useful to do when you have recently acquired new music." + (interactive + (list (if emms-player-mpd-music-directory + (emms-read-directory-name "Directory: " + emms-player-mpd-music-directory) + (read-string "Directory: ")))) + (unless (string= dir "") + (setq dir (emms-player-mpd-get-mpd-filename dir))) + (if emms-cache-set-function + (progn + (message "Dumping MusicPD data to cache...") + (emms-player-mpd-send + (concat "listallinfo " dir) + nil + (lambda (closure response) + (message "Dumping MusicPD data to cache...processing") + (let ((info (emms-player-mpd-parse-response response))) + (when (null (car info)) + (let* ((info (emms-cache--info-cleanup info)) + (info (emms-player-mpd-get-alists info)) + (track 1) + (total (length info))) + (dolist (track-info info) + (message "Dumping MusicPD data to cache...%d/%d" track total) + (emms-cache-set-from-mpd-track track-info) + (setq track (+ 1 track))) + (message "Dumping MusicPD data to cache... %d tracks processed" + total))))))) + (error "Caching is not enabled"))) + +(defun emms-cache-set-from-mpd-all () + "Dump all MusicPD data into the EMMS cache. + +This is useful to do once, just before using emms-browser.el, in +order to prime the cache." + (interactive) + (emms-cache-set-from-mpd-directory "")) + +;;; Updating tracks + +(defun emms-player-mpd-update-directory (dir) + "Cause the tracks in DIR to be updated in the MusicPD database." + (interactive + (list (if emms-player-mpd-music-directory + (emms-read-directory-name "Directory: " + emms-player-mpd-music-directory) + (read-string "Directory: ")))) + (unless (string= dir "") + (setq dir (emms-player-mpd-get-mpd-filename dir))) + (emms-player-mpd-send + (concat "update " (emms-player-mpd-quote-file dir)) nil + (lambda (closure response) + (let ((id (cdr (assoc "updating_db" + (emms-player-mpd-get-alist + (emms-player-mpd-parse-response response)))))) + (if id + (message "Updating DB with ID %s" id) + (message "Could not update the DB")))))) + +(defun emms-player-mpd-update-all () + "Cause all tracks in the MusicPD music directory to be updated in +the MusicPD database." + (interactive) + (emms-player-mpd-update-directory "")) + +(defvar emms-player-mpd-waiting-for-update-timer nil + "Timer object when waiting for MPD update to finish.") + +(defun emms-player-mpd-update-all-reset-cache () + "Update all tracks in the MusicPD music directory. +When update finishes, clear the EMMS cache and call +`emms-cache-set-from-mpd-all' to dump the MusicPD data into the +cache." + (interactive) + (if emms-player-mpd-waiting-for-update-timer + (message "Already waiting for an update to finish.") + (emms-player-mpd-send + "update" nil + 'emms-player-mpd-wait-for-update))) + +(defun emms-player-mpd-wait-for-update (&optional closure response) + "Wait for a currently running mpd update to finish. +Afterwards, clear the EMMS cache and call +`emms-cache-set-from-mpd-all'." + (if response + ;; This is the first call after the update command + (let ((id (cdr (assoc "updating_db" + (emms-player-mpd-get-alist + (emms-player-mpd-parse-response response)))))) + (if id + (progn + (message "Updating DB with ID %s. Waiting for the update to finish..." id) + (setq emms-player-mpd-waiting-for-update-timer + (run-at-time 1 nil 'emms-player-mpd-wait-for-update))) + (message "Could not update the DB"))) + ;; Otherwise, check if update is still in progress + (emms-player-mpd-get-status-part + nil + (lambda (closure updating) + (if updating + ;; MPD update still in progress, so wait another second + (run-at-time 1 nil 'emms-player-mpd-wait-for-update) + ;; MPD update finished + (setq emms-player-mpd-waiting-for-update-timer nil) + (message "MPD update finished.") + (sit-for 1) + (clrhash emms-cache-db) + (emms-cache-set-from-mpd-all))) + "updating_db"))) + + +(provide 'emms-player-mpd) + +;;; emms-player-mpd.el ends here -- cgit v1.2.3