aboutsummaryrefslogtreecommitdiff
path: root/emms.el
diff options
context:
space:
mode:
Diffstat (limited to 'emms.el')
-rw-r--r--emms.el1528
1 files changed, 1518 insertions, 10 deletions
diff --git a/emms.el b/emms.el
index e43457e..1f49e73 100644
--- a/emms.el
+++ b/emms.el
@@ -5,7 +5,7 @@
;; Author: Jorgen Schäfer <forcer@forcix.cx>, the Emms developers (see AUTHORS file)
;; Maintainer: Yoni Rabkin <yrk@gnu.org>
-;; Version: 5.4
+;; Version: 5.41
;; Keywords: emms, mp3, ogg, flac, music, mpeg, video, multimedia
;; Package-Type: multi
;; Package-Requires: ((cl-lib "0.5"))
@@ -13,20 +13,1528 @@
;; 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 of the License, or
-;; (at your option) any later version.
+;; 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.
+;; 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 this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
+;; This is the very core of EMMS. It provides ways to play a track
+;; using `emms-start', to go through the playlist using the commands
+;; `emms-next' and `emms-previous', to stop the playback using
+;; `emms-stop', and to see what's currently playing using `emms-show'.
-;;; emms.el ends here.
+;; But in itself, this core is useless, because it doesn't know how to
+;; play any tracks --- you need players for this. In fact, it doesn't
+;; even know how to find any tracks to consider playing --- for this,
+;; you need sources.
+
+;; A sample configuration is offered in emms-setup.el, and the
+;; Friendly Manual in the doc/ directory is both detailed, and kept up
+;; to date.
+
+
+;;; Code:
+
+(defvar emms-version "5.41"
+ "EMMS version string.")
+
+
+;;; User Customization
+
+(defgroup emms nil
+ "*The Emacs Multimedia System."
+ :prefix "emms-"
+ :group 'multimedia
+ :group 'applications)
+
+(defgroup emms-player nil
+ "*Track players for EMMS."
+ :prefix "emms-player-"
+ :group 'emms)
+
+(defgroup emms-source nil
+ "*Track sources for EMMS."
+ :prefix "emms-source-"
+ :group 'emms)
+
+(defcustom emms-player-list nil
+ "*List of players that EMMS can use. You need to set this!"
+ :group 'emms
+ :type '(repeat (symbol :tag "Player")))
+
+(defcustom emms-show-format "Currently playing: %s"
+ "*The format to use for `emms-show'.
+Any \"%s\" is replaced by what `emms-track-description-function' returns
+for the currently playing track."
+ :group 'emms
+ :type 'string)
+
+(defcustom emms-repeat-playlist nil
+ "*Non-nil if the EMMS playlist should automatically repeat.
+If nil, playback will stop when the last track finishes playing.
+If non-nil, EMMS will wrap back to the first track when that happens."
+ :group 'emms
+ :type 'boolean)
+
+(defcustom emms-random-playlist nil
+ "*Non-nil means that tracks are played randomly. If nil, tracks
+are played sequentially."
+ :group 'emms
+ :type 'boolean)
+
+(defcustom emms-repeat-track nil
+ "Non-nil, playback will repeat current track. If nil, EMMS will play
+track by track normally."
+ :group 'emms
+ :type 'boolean)
+
+(defvar-local emms-single-track nil
+ "Non-nil, play the current track and then stop.")
+
+(defcustom emms-completing-read-function
+ (if (and (boundp 'ido-mode)
+ ido-mode)
+ 'ido-completing-read
+ 'completing-read)
+ "Function to call when prompting user to choose between a list of options.
+This should take the same arguments as `completing-read'. Some
+possible values are `completing-read' and `ido-completing-read'.
+Note that you must set `ido-mode' if using
+`ido-completing-read'."
+ :group 'emms
+ :type 'function)
+
+(defcustom emms-track-description-function 'emms-track-simple-description
+ "*Function for describing an EMMS track in a user-friendly way."
+ :group 'emms
+ :type 'function)
+
+(defcustom emms-player-delay 0
+ "The delay to pause after a player finished.
+This is a floating-point number of seconds. This is necessary
+for some platforms where it takes a bit to free the audio device
+after a player has finished. If EMMS is skipping songs, increase
+this number."
+ :type 'number
+ :group 'emms)
+
+(defcustom emms-playlist-shuffle-function 'emms-playlist-simple-shuffle
+ "*The function to use for shuffling the playlist."
+ :type 'function
+ :group 'emms)
+
+(defcustom emms-playlist-sort-function 'emms-playlist-simple-sort
+ "*The function to use for sorting the playlist."
+ :type 'function
+ :group 'emms)
+
+(defcustom emms-playlist-uniq-function 'emms-playlist-simple-uniq
+ "*The function to use for removing duplicate tracks in the playlist."
+ :type 'function
+ :group 'emms)
+
+(defcustom emms-sort-lessp-function 'emms-sort-track-name-less-p
+ "*Function for comparing two EMMS tracks.
+The function should return non-nil if and only if the first track
+sorts before the second (see `sort')."
+ :group 'emms
+ :type 'function)
+
+(defcustom emms-playlist-buffer-name " *EMMS Playlist*"
+ "*The default name of the EMMS playlist buffer."
+ :type 'string
+ :group 'emms)
+
+(defcustom emms-playlist-default-major-mode 'emms-playlist-mode
+ "*The default major mode for EMMS playlist."
+ :type 'function
+ :group 'emms)
+
+(defcustom emms-playlist-insert-track-function 'emms-playlist-simple-insert-track
+ "*A function to insert a track into the playlist buffer."
+ :group 'emms
+ :type 'function)
+(make-variable-buffer-local 'emms-playlist-insert-track-function)
+
+(defcustom emms-playlist-update-track-function 'emms-playlist-simple-update-track
+ "*A function to update the track at point.
+This is called when the track information changed. This also
+shouldn't assume that the track has been inserted before."
+ :group 'emms
+ :type 'function)
+(make-variable-buffer-local 'emms-playlist-insert-track-function)
+
+(defcustom emms-playlist-delete-track-function 'emms-playlist-simple-delete-track
+ "*A function to delete the track at point in the playlist buffer."
+ :group 'emms
+ :type 'function)
+(make-variable-buffer-local 'emms-playlist-delete-track-function)
+
+(defcustom emms-ok-track-function 'emms-default-ok-track-function
+ "*Function returns true if we shouldn't skip this track."
+ :group 'emms
+ :type 'function)
+
+(defcustom emms-playlist-source-inserted-hook nil
+ "*Hook run when a source got inserted into the playlist.
+The buffer is narrowed to the new tracks."
+ :type 'hook
+ :group 'emms)
+
+(defcustom emms-playlist-selection-changed-hook nil
+ "*Hook run after another track is selected in the EMMS playlist."
+ :group 'emms
+ :type 'hook)
+
+(defcustom emms-playlist-cleared-hook nil
+ "*Hook run after the current EMMS playlist is cleared.
+This happens both when the playlist is cleared and when a new
+buffer is created for it."
+ :group 'emms
+ :type 'hook)
+
+(defcustom emms-track-initialize-functions nil
+ "*List of functions to call for each new EMMS track.
+This can be used to initialize tracks with various info."
+ :group 'emms
+ :type 'hook)
+
+(defcustom emms-track-info-filters nil
+ "*List of functions to call when a track changes data, before updating
+the display.
+These functions are passed the track as an argument."
+ :group 'emms
+ :type 'hook)
+
+(defcustom emms-track-updated-functions nil
+ "*List of functions to call when a track changes data, after updating
+the display.
+These functions are passed the track as an argument."
+ :group 'emms
+ :type 'hook)
+
+(defcustom emms-player-started-hook nil
+ "*Hook run when an EMMS player starts playing."
+ :group 'emms
+ :type 'hook
+ :options '(emms-show))
+
+(defcustom emms-player-stopped-hook nil
+ "*Hook run when an EMMS player is stopped by the user.
+See `emms-player-finished-hook'."
+ :group 'emms
+ :type 'hook)
+
+(defcustom emms-player-finished-hook nil
+ "*Hook run when an EMMS player finishes playing a track.
+Please pay attention to the differences between
+`emms-player-finished-hook' and `emms-player-stopped-hook'. The
+former is called only when the player actually finishes playing a
+track; the latter, only when the player is stopped
+interactively."
+ :group 'emms
+ :type 'hook)
+
+(defcustom emms-player-next-function 'emms-next-noerror
+ "*A function run when EMMS thinks the next song should be played."
+ :group 'emms
+ :type 'function
+ :options '(emms-next-noerror
+ emms-random))
+
+(defcustom emms-player-paused-hook nil
+ "*Hook run when a player is paused or resumed.
+Use `emms-player-paused-p' to find the current state."
+ :group 'emms
+ :type 'hook)
+
+(defcustom emms-seek-seconds 10
+ "The number of seconds to seek forward or backward when seeking."
+ :group 'emms
+ :type 'number)
+
+(defcustom emms-player-seeked-functions nil
+ "*Functions called when a player is seeking.
+The functions are called with a single argument, the amount of
+seconds the player did seek."
+ :group 'emms
+ :type 'hook)
+
+(defcustom emms-player-time-set-functions nil
+ "*Functions called when a player is setting the elapsed time of a track.
+The functions are called with a single argument, the time elapsed
+since the beginning of the current track."
+ :group 'emms
+ :type 'hook)
+
+(defcustom emms-cache-get-function nil
+ "A function to retrieve a track entry from the cache.
+This is called with two arguments, the type and the name."
+ :group 'emms
+ :type 'function)
+
+(defcustom emms-cache-set-function nil
+ "A function to add/set a track entry from the cache.
+This is called with three arguments: the type of the track, the
+name of the track, and the track itself."
+ :group 'emms
+ :type 'function)
+
+(defcustom emms-cache-modified-function nil
+ "A function to be called when a track is modified.
+The modified track is passed as the argument to this function."
+ :group 'emms
+ :type 'function)
+
+(defcustom emms-directory (expand-file-name "emms" user-emacs-directory)
+ "*Directory variable from which all other emms file variables are derived."
+ :group 'emms
+ :type 'string)
+
+(defvar emms-player-playing-p nil
+ "The currently playing EMMS player, or nil.")
+
+(defvar emms-player-paused-p nil
+ "Whether the current player is paused or not.")
+
+(defvar emms-source-old-buffer nil
+ "The active buffer before a source was invoked.
+This can be used if the source depends on the current buffer not
+being the playlist buffer.")
+
+(defvar emms-playlist-buffer nil
+ "The current playlist buffer, if any.")
+
+
+;;; Macros
+
+;;; These need to be at the top of the file so that compilation works.
+
+(defmacro with-current-emms-playlist (&rest body)
+ "Run BODY with the current buffer being the current playlist buffer.
+This also disables any read-onliness of the current buffer."
+ `(progn
+ (when (or (not emms-playlist-buffer)
+ (not (buffer-live-p emms-playlist-buffer)))
+ (emms-playlist-current-clear))
+ (let ((emms-source-old-buffer (or emms-source-old-buffer
+ (current-buffer))))
+ (with-current-buffer emms-playlist-buffer
+ (let ((inhibit-read-only t))
+ ,@body)))))
+(put 'with-current-emms-playlist 'lisp-indent-function 0)
+(put 'with-current-emms-playlist 'edebug-form-spec '(body))
+
+(defmacro emms-with-inhibit-read-only-t (&rest body)
+ "Simple wrapper around `inhibit-read-only'."
+ `(let ((inhibit-read-only t))
+ ,@body))
+(put 'emms-with-inhibit-read-only-t 'edebug-form-spec '(body))
+
+(defmacro emms-with-widened-buffer (&rest body)
+ `(save-restriction
+ (widen)
+ ,@body))
+(put 'emms-with-widened-buffer 'edebug-form-spec '(body))
+
+(defmacro emms-walk-tracks (&rest body)
+ "Execute BODY for each track in the current buffer, starting at point.
+Point will be placed at the beginning of the track before
+executing BODY.
+
+Point will not be restored afterward."
+ (let ((donep (make-symbol "donep")))
+ `(let ((,donep nil))
+ ;; skip to first track if not on one
+ (unless (emms-playlist-track-at (point))
+ (condition-case nil
+ (emms-playlist-next)
+ (error
+ (setq ,donep t))))
+ ;; walk tracks
+ (while (not ,donep)
+ ,@body
+ (condition-case nil
+ (emms-playlist-next)
+ (error
+ (setq ,donep t)))))))
+(put 'emms-walk-tracks 'lisp-indent-function 0)
+(put 'emms-walk-tracks 'edebug-form-spec '(body))
+
+(defvar emms-player-base-format-list
+ '("ogg" "mp3" "wav" "mpg" "mpeg" "wmv" "wma"
+ "mov" "avi" "divx" "ogm" "ogv" "asf" "mkv"
+ "rm" "rmvb" "mp4" "flac" "vob" "m4a" "ape"
+ "flv" "webm" "aif")
+ "A list of common formats which player definitions can use.")
+
+
+;;; User Interface
+
+(defun emms-start ()
+ "Start playing the current track in the EMMS playlist."
+ (interactive)
+ (unless emms-player-playing-p
+ (emms-player-start (emms-playlist-current-selected-track))))
+
+(defun emms-stop ()
+ "Stop any current EMMS playback."
+ (interactive)
+ (when emms-player-playing-p
+ (emms-player-stop)))
+
+(defun emms-next ()
+ "Start playing the next track in the EMMS playlist.
+This might behave funny if called from `emms-player-next-function',
+so use `emms-next-noerror' in that case."
+ (interactive)
+ (when emms-player-playing-p
+ (emms-stop))
+ (emms-playlist-current-select-next)
+ (emms-start))
+
+(defun emms-next-noerror ()
+ "Start playing the next track in the EMMS playlist.
+Unlike `emms-next', this function doesn't signal an error when called
+at the end of the playlist.
+This function should only be called when no player is playing.
+This is a good function to put in `emms-player-next-function'."
+ (interactive)
+ (when emms-player-playing-p
+ (error "A track is already being played"))
+ (cond (emms-repeat-track
+ (emms-start))
+ (emms-single-track ; buffer local
+ (emms-stop))
+ ;; attempt to play the next track but ignore errors
+ ((condition-case nil
+ (progn
+ (emms-playlist-current-select-next)
+ t)
+ (error nil))
+ (if (funcall emms-ok-track-function
+ (emms-playlist-current-selected-track))
+ (emms-start)
+ (emms-next-noerror)))
+ (t
+ (message "No next track in playlist"))))
+
+(defun emms-previous ()
+ "Start playing the previous track in the EMMS playlist."
+ (interactive)
+ (when emms-player-playing-p
+ (emms-stop))
+ (emms-playlist-current-select-previous)
+ (emms-start))
+
+(defun emms-random ()
+ "Jump to a random track."
+ (interactive)
+ (when emms-player-playing-p
+ (emms-stop))
+ (emms-playlist-current-select-random)
+ (emms-start))
+
+(defun emms-pause ()
+ "Pause the current player.
+If player hasn't started, then start it now."
+ (interactive)
+ (if emms-player-playing-p
+ (emms-player-pause)
+ (emms-start)))
+
+(defun emms-seek (seconds)
+ "Seek the current player SECONDS seconds.
+This can be a floating point number for sub-second fractions.
+It can also be negative to seek backwards."
+ (interactive "nSeconds to seek: ")
+ (emms-ensure-player-playing-p)
+ (emms-player-seek seconds))
+
+(defun emms-seek-to (seconds)
+ "Seek the current player to SECONDS seconds.
+This can be a floating point number for sub-second fractions.
+It can also be negative to seek backwards."
+ (interactive "nSeconds to seek to: ")
+ (emms-ensure-player-playing-p)
+ (emms-player-seek-to seconds))
+
+(defun emms-seek-forward ()
+ "Seek ten seconds forward."
+ (interactive)
+ (when emms-player-playing-p
+ (emms-player-seek emms-seek-seconds)))
+
+(defun emms-seek-backward ()
+ "Seek ten seconds backward."
+ (interactive)
+ (when emms-player-playing-p
+ (emms-player-seek (- emms-seek-seconds))))
+
+(defun emms-show (&optional insertp)
+ "Describe the current EMMS track in the minibuffer.
+If INSERTP is non-nil, insert the description into the current buffer instead.
+This function uses `emms-show-format' to format the current track."
+ (interactive "P")
+ (let ((string (if emms-player-playing-p
+ (format emms-show-format
+ (emms-track-description
+ (emms-playlist-current-selected-track)))
+ "Nothing playing right now")))
+ (if insertp
+ (insert string)
+ (message "%s" string))))
+
+(defun emms-shuffle ()
+ "Shuffle the current playlist.
+This uses `emms-playlist-shuffle-function'."
+ (interactive)
+ (with-current-emms-playlist
+ (save-excursion
+ (funcall emms-playlist-shuffle-function))))
+
+(defun emms-sort ()
+ "Sort the current playlist.
+This uses `emms-playlist-sort-function'."
+ (interactive)
+ (with-current-emms-playlist
+ (save-excursion
+ (funcall emms-playlist-sort-function))))
+
+(defun emms-uniq ()
+ "Remove duplicates from the current playlist.
+This uses `emms-playlist-uniq-function'."
+ (interactive)
+ (with-current-emms-playlist
+ (save-excursion
+ (funcall emms-playlist-uniq-function))))
+
+(defun emms-toggle-single-track ()
+ "Toggle if Emms plays a single track and stops."
+ (interactive)
+ (with-current-emms-playlist
+ (cond (emms-single-track
+ (setq emms-single-track nil)
+ (message "single track mode disabled for %s"
+ (buffer-name)))
+ (t (setq emms-single-track t)
+ (message "single track mode enabled for %s"
+ (buffer-name))))))
+
+(defun emms-toggle-random-playlist ()
+ "Toggle whether emms plays the tracks randomly or sequentially.
+See `emms-random-playlist'."
+ (interactive)
+ (setq emms-random-playlist (not emms-random-playlist))
+ (if emms-random-playlist
+ (progn (setq emms-player-next-function 'emms-random)
+ (message "Will play the tracks randomly."))
+ (setq emms-player-next-function 'emms-next-noerror)
+ (message "Will play the tracks sequentially.")))
+
+(defun emms-toggle-repeat-playlist ()
+ "Toggle whether emms repeats the playlist after it is done.
+See `emms-repeat-playlist'."
+ (interactive)
+ (setq emms-repeat-playlist (not emms-repeat-playlist))
+ (if emms-repeat-playlist
+ (message "Will repeat the playlist after it is done.")
+ (message "Will stop after the playlist is over.")))
+
+(defun emms-toggle-repeat-track ()
+ "Toggle whether emms repeats the current track.
+See `emms-repeat-track'."
+ (interactive)
+ (setq emms-repeat-track (not emms-repeat-track))
+ (if emms-repeat-track
+ (message "Will repeat the current track.")
+ (message "Will advance to the next track after this one.")))
+
+(defun emms-sort-track-name-less-p (a b)
+ "Return non-nil if the track name of A sorts before B."
+ (string< (emms-track-name a)
+ (emms-track-name b)))
+
+(defun emms-ensure-player-playing-p ()
+ "Raise an error if no player is playing right now."
+ (when (not emms-player-playing-p)
+ (error "No EMMS player playing right now")))
+
+(defun emms-completing-read (&rest args)
+ "Read a string in the minibuffer, with completion.
+Set `emms-completing-read' to determine which function to use.
+
+See `completing-read' for a description of ARGS."
+ (apply emms-completing-read-function args))
+
+(defun emms-display-modes ()
+ "Display the current EMMS play modes."
+ (interactive)
+ (with-current-emms-playlist
+ (message
+ "repeat playlist: %s, repeat track: %s, random: %s, single %s"
+ (if emms-repeat-playlist "yes" "no")
+ (if emms-repeat-track "yes" "no")
+ (if emms-random-playlist "yes" "no")
+ (if emms-single-track "yes" "no"))))
+
+
+;;; Compatibility functions
+
+(require 'emms-compat)
+
+
+;;; Utility functions
+
+(defun emms-insert-file-contents (filename &optional visit)
+ "Insert the contents of file FILENAME after point.
+Do character code conversion and end-of-line conversion, but none
+of the other unnecessary things like format decoding or
+`find-file-hook'.
+
+If VISIT is non-nil, the buffer's visited filename
+and last save file modtime are set, and it is marked unmodified.
+If visiting and the file does not exist, visiting is completed
+before the error is signaled."
+ (let ((format-alist nil)
+ (after-insert-file-functions nil)
+ (inhibit-file-name-handlers
+ (append '(jka-compr-handler image-file-handler epa-file-handler)
+ inhibit-file-name-handlers))
+ (inhibit-file-name-operation 'insert-file-contents))
+ (insert-file-contents filename visit)))
+
+
+;;; Dictionaries
+
+;; This is a simple helper data structure, used by both players
+;; and tracks.
+
+(defsubst emms-dictionary (name)
+ "Create a new dictionary of type NAME."
+ (list name))
+
+(defsubst emms-dictionary-type (dict)
+ "Return the type of the dictionary DICT."
+ (car dict))
+
+(defun emms-dictionary-get (dict name &optional default)
+ "Return the value of NAME in DICT."
+ (let ((item (assq name (cdr dict))))
+ (if item
+ (cdr item)
+ default)))
+
+(defun emms-dictionary-set (dict name value)
+ "Set the value of NAME in DICT to VALUE."
+ (let ((item (assq name (cdr dict))))
+ (if item
+ (setcdr item value)
+ (setcdr dict (append (cdr dict)
+ (list (cons name value))))))
+ dict)
+
+
+;;; Tracks
+
+;; This is a simple datatype to store track information.
+;; Each track consists of a type (a symbol) and a name (a string).
+;; In addition, each track has an associated dictionary of information.
+
+(defun emms-track (type name)
+ "Create an EMMS track with type TYPE and name NAME."
+ (let ((track (when emms-cache-get-function
+ (funcall emms-cache-get-function type name))))
+ (when (not track)
+ (setq track (emms-dictionary '*track*))
+ ;; Prevent the cache from being called for these two sets
+ (let ((emms-cache-modified-function nil))
+ (emms-track-set track 'type type)
+ (emms-track-set track 'name name))
+ (when emms-cache-set-function
+ (funcall emms-cache-set-function type name track)))
+ ;; run any hooks regardless of a cache hit, as the entry may be
+ ;; old
+ (run-hook-with-args 'emms-track-initialize-functions track)
+ track))
+
+(defun emms-track-p (obj)
+ "True if OBJ is an emms track."
+ (and (listp obj)
+ (eq (car obj) '*track*)))
+
+(defun emms-track-type (track)
+ "Return the type of TRACK."
+ (emms-track-get track 'type))
+
+(defun emms-track-name (track)
+ "Return the name of TRACK."
+ (emms-track-get track 'name))
+
+(defun emms-track-get (track name &optional default)
+ "Return the value of NAME for TRACK.
+If there is no value, return DEFAULT (or nil, if not given)."
+ (emms-dictionary-get track name default))
+
+(defun emms-track-set (track name value)
+ "Set the value of NAME for TRACK to VALUE."
+ (emms-dictionary-set track name value)
+ (when emms-cache-modified-function
+ (funcall emms-cache-modified-function track)))
+
+(defun emms-track-description (track)
+ "Return a description of TRACK.
+This function uses the global value for
+`emms-track-description-function', rather than anything the
+current mode might have set.
+
+Use `emms-track-force-description' instead if you need to insert
+a description into a playlist buffer."
+ (funcall (default-value 'emms-track-description-function) track))
+
+(defun emms-track-updated (track)
+ "Information in TRACK got updated."
+ (run-hook-with-args 'emms-track-info-filters track)
+ (emms-playlist-track-updated track)
+ (run-hook-with-args 'emms-track-updated-functions track))
+
+(defun emms-track-simple-description (track)
+ "Simple function to give a user-readable description of a track.
+If it's a file track, just return the file name. Otherwise,
+return the type and the name with a colon in between.
+Hex-encoded characters in URLs are replaced by the decoded
+character."
+ (let ((type (emms-track-type track)))
+ (cond ((eq 'file type)
+ (emms-track-name track))
+ ((eq 'url type)
+ (emms-format-url-track-name (emms-track-name track)))
+ (t (concat (symbol-name type)
+ ": " (emms-track-name track))))))
+
+(defun emms-format-url-track-name (name)
+ "Format URL track name for better readability."
+ (url-unhex-string name))
+
+(defun emms-track-force-description (track)
+ "Always return text that describes TRACK.
+This is used when inserting a description into a buffer.
+
+The reason for this is that if no text was returned (i.e. the
+user defined a track function that returned nil or the empty
+string), a confusing error message would result."
+ (let ((desc (funcall emms-track-description-function track)))
+ (if (and (stringp desc) (not (string= desc "")))
+ desc
+ (emms-track-simple-description track))))
+
+(defun emms-track-get-year (track)
+ "Get year of TRACK for display.
+There is the separation between the 'release date' and the
+'original date'. This difference matters e.g. for
+re-releases (anniversaries and such) where the release date is
+more recent than the original release date. In such cases the
+user probably wants the original release date so this is what we
+show."
+ (or
+ (emms-format-date-to-year (emms-track-get track 'info-date))
+ (emms-format-date-to-year (emms-track-get track 'info-originaldate))
+ (emms-track-get track 'info-year)
+ (emms-track-get track 'info-originalyear)))
+
+(defun emms-format-date-to-year (date)
+ "Try to extract year part from DATE.
+Return nil if the year cannot be extracted."
+ (when date
+ (let ((year (nth 5 (parse-time-string date))))
+ (if year (number-to-string year)
+ (when (string-match "^[ \t]*\\([0-9]\\{4\\}\\)" date)
+ (match-string 1 date))))))
+
+
+;;; The Playlist
+
+;; Playlists are stored in buffers. The current playlist buffer is
+;; remembered in the `emms-playlist' variable. The buffer consists of
+;; any kind of data. Strings of text with a `emms-track' property are
+;; the tracks in the buffer.
+
+(defvar emms-playlist-buffers nil
+ "The list of EMMS playlist buffers.
+You should use the `emms-playlist-buffer-list' function to
+retrieve a current list of EMMS buffers. Never use this variable
+for that purpose.")
+
+(defvar emms-playlist-selected-marker nil
+ "The marker for the currently selected track.")
+(make-variable-buffer-local 'emms-playlist-selected-marker)
+
+(defvar emms-playlist-buffer-p nil
+ "Non-nil if the current buffer is an EMMS playlist.")
+(make-variable-buffer-local 'emms-playlist-buffer-p)
+
+(defun emms-playlist-ensure-playlist-buffer ()
+ "Throw an error if we're not in a playlist-buffer."
+ (when (not emms-playlist-buffer-p)
+ (error "Not an EMMS playlist buffer")))
+
+(defun emms-playlist-set-playlist-buffer (&optional buffer)
+ "Set the current playlist buffer."
+ (interactive
+ (list (let* ((buf-list (mapcar #'(lambda (buf)
+ (list (buffer-name buf)))
+ (emms-playlist-buffer-list)))
+ (sorted-buf-list (sort buf-list
+ #'(lambda (lbuf rbuf)
+ (< (length (car lbuf))
+ (length (car rbuf))))))
+ (default (or (and emms-playlist-buffer-p
+ ;; default to current buffer
+ (buffer-name))
+ ;; pick shortest buffer name, since it is
+ ;; likely to be a shared prefix
+ (car sorted-buf-list))))
+ (emms-completing-read "Playlist buffer to make current: "
+ sorted-buf-list nil t default))))
+ (let ((buf (if buffer
+ (get-buffer buffer)
+ (current-buffer))))
+ (with-current-buffer buf
+ (emms-playlist-ensure-playlist-buffer))
+ (setq emms-playlist-buffer buf)
+ (when (called-interactively-p 'interactive)
+ (message "Set current EMMS playlist buffer"))
+ buf))
+
+(defun emms-playlist-new (&optional name)
+ "Create a new playlist buffer.
+The buffer is named NAME, but made unique. NAME defaults to
+`emms-playlist-buffer-name'. If called interactively, the new
+buffer is also selected."
+ (interactive)
+ (let ((buf (generate-new-buffer (or name
+ emms-playlist-buffer-name))))
+ (with-current-buffer buf
+ (when (not (eq major-mode emms-playlist-default-major-mode))
+ (funcall emms-playlist-default-major-mode))
+ (setq emms-playlist-buffer-p t))
+ (add-to-list 'emms-playlist-buffers buf)
+ (when (called-interactively-p 'interactive)
+ (switch-to-buffer buf))
+ buf))
+
+(defun emms-playlist-buffer-list ()
+ "Return a list of EMMS playlist buffers.
+The first element is guaranteed to be the current EMMS playlist
+buffer, if it exists, otherwise the slot will be used for the
+other EMMS buffers. The list will be in newest-first order."
+ ;; prune dead buffers
+ (setq emms-playlist-buffers (emms-delete-if (lambda (buf)
+ (not (buffer-live-p buf)))
+ emms-playlist-buffers))
+ ;; add new buffers
+ (mapc (lambda (buf)
+ (when (buffer-live-p buf)
+ (with-current-buffer buf
+ (when (and emms-playlist-buffer-p
+ (not (memq buf emms-playlist-buffers)))
+ (setq emms-playlist-buffers
+ (cons buf emms-playlist-buffers))))))
+ (buffer-list))
+ ;; force current playlist buffer to head position
+ (when (and (buffer-live-p emms-playlist-buffer)
+ (not (eq (car emms-playlist-buffers) emms-playlist-buffer)))
+ (setq emms-playlist-buffers (cons emms-playlist-buffer
+ (delete emms-playlist-buffer
+ emms-playlist-buffers))))
+ emms-playlist-buffers)
+
+(defun emms-playlist-current-kill ()
+ "Kill the current EMMS playlist buffer and switch to the next one."
+ (interactive)
+ (when (buffer-live-p emms-playlist-buffer)
+ (let ((new (cadr (emms-playlist-buffer-list))))
+ (if new
+ (let ((old emms-playlist-buffer))
+ (setq emms-playlist-buffer new
+ emms-playlist-buffers (cdr emms-playlist-buffers))
+ (kill-buffer old)
+ (switch-to-buffer emms-playlist-buffer))
+ (with-current-buffer emms-playlist-buffer
+ (bury-buffer))))))
+
+(defun emms-playlist-current-clear ()
+ "Clear the current playlist.
+If no current playlist exists, a new one is generated."
+ (interactive)
+ (if (or (not emms-playlist-buffer)
+ (not (buffer-live-p emms-playlist-buffer)))
+ (setq emms-playlist-buffer (emms-playlist-new))
+ (with-current-buffer emms-playlist-buffer
+ (emms-playlist-clear))))
+
+(defun emms-playlist-clear ()
+ "Clear the current buffer."
+ (interactive)
+ (emms-playlist-ensure-playlist-buffer)
+ (let ((inhibit-read-only t))
+ (widen)
+ (delete-region (point-min)
+ (point-max)))
+ (run-hooks 'emms-playlist-cleared-hook))
+
+;;; Point movement within the playlist buffer.
+(defun emms-playlist-track-at (&optional pos)
+ "Return the track at POS (point if not given), or nil if none."
+ (emms-playlist-ensure-playlist-buffer)
+ (emms-with-widened-buffer
+ (get-text-property (or pos (point))
+ 'emms-track)))
+
+(defun emms-playlist-next ()
+ "Move to the next track in the current buffer."
+ (emms-playlist-ensure-playlist-buffer)
+ (let ((next (next-single-property-change (point)
+ 'emms-track)))
+ (when (not next)
+ (error "No next track"))
+ (when (not (emms-playlist-track-at next))
+ (setq next (next-single-property-change next 'emms-track)))
+ (when (or (not next)
+ (= next (point-max)))
+ (error "No next track"))
+ (goto-char next)))
+
+(defun emms-playlist-previous ()
+ "Move to the previous track in the current buffer."
+ (emms-playlist-ensure-playlist-buffer)
+ (let ((prev (previous-single-property-change (point)
+ 'emms-track)))
+ (when (not prev)
+ (error "No previous track"))
+ (when (not (get-text-property prev 'emms-track))
+ (setq prev (or (previous-single-property-change prev 'emms-track)
+ (point-min))))
+ (when (or (not prev)
+ (not (get-text-property prev 'emms-track)))
+ (error "No previous track"))
+ (goto-char prev)))
+
+(defun emms-playlist-first ()
+ "Move to the first track in the current buffer."
+ (emms-playlist-ensure-playlist-buffer)
+ (let ((first (condition-case nil
+ (save-excursion
+ (goto-char (point-min))
+ (when (not (emms-playlist-track-at (point)))
+ (emms-playlist-next))
+ (point))
+ (error
+ nil))))
+ (if first
+ (goto-char first)
+ (error "No first track"))))
+
+(defun emms-playlist-last ()
+ "Move to the last track in the current buffer."
+ (emms-playlist-ensure-playlist-buffer)
+ (let ((last (condition-case nil
+ (save-excursion
+ (goto-char (point-max))
+ (emms-playlist-previous)
+ (point))
+ (error
+ nil))))
+ (if last
+ (goto-char last)
+ (error "No last track"))))
+
+(defun emms-playlist-delete-track ()
+ "Delete the track at point."
+ (emms-playlist-ensure-playlist-buffer)
+ (funcall emms-playlist-delete-track-function))
+
+;;; Track selection
+(defun emms-playlist-selected-track ()
+ "Return the currently selected track."
+ (emms-playlist-ensure-playlist-buffer)
+ (when emms-playlist-selected-marker
+ (emms-playlist-track-at emms-playlist-selected-marker)))
+
+(defun emms-playlist-current-selected-track ()
+ "Return the currently selected track in the current playlist."
+ (with-current-emms-playlist
+ (emms-playlist-selected-track)))
+
+(defun emms-playlist-selected-track-at-p (&optional point)
+ "Return non-nil if POINT (defaulting to point) is on the selected track."
+ (when emms-playlist-selected-marker
+ (or (= emms-playlist-selected-marker
+ (or point (point)))
+ (let ((p (previous-single-property-change (or point (point))
+ 'emms-track)))
+ (when p
+ (= emms-playlist-selected-marker
+ p))))))
+
+(defun emms-playlist-select (pos)
+ "Select the track at POS."
+ (emms-playlist-ensure-playlist-buffer)
+ (when (not (emms-playlist-track-at pos))
+ (error "No track at position %s" pos))
+ (when (not emms-playlist-selected-marker)
+ (setq emms-playlist-selected-marker (make-marker)))
+ (set-marker-insertion-type emms-playlist-selected-marker t)
+ (set-marker emms-playlist-selected-marker pos)
+ (run-hooks 'emms-playlist-selection-changed-hook))
+
+(defun emms-playlist-select-next ()
+ "Select the next track in the current buffer."
+ (emms-playlist-ensure-playlist-buffer)
+ (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
+ (progn
+ (if emms-repeat-playlist
+ (condition-case nil
+ (emms-playlist-next)
+ (error
+ (emms-playlist-first)))
+ (emms-playlist-next))
+ (emms-playlist-select (point)))
+ (error
+ (error "No next track in playlist")))))
+
+(defun emms-playlist-current-select-next ()
+ "Select the next track in the current playlist."
+ (with-current-emms-playlist
+ (emms-playlist-select-next)))
+
+(defun emms-playlist-select-previous ()
+ "Select the previous track in the current buffer."
+ (emms-playlist-ensure-playlist-buffer)
+ (save-excursion
+ (goto-char (if (and emms-playlist-selected-marker
+ (marker-position emms-playlist-selected-marker))
+ emms-playlist-selected-marker
+ (point-max)))
+ (condition-case nil
+ (progn
+ (if emms-repeat-playlist
+ (condition-case nil
+ (emms-playlist-previous)
+ (error
+ (emms-playlist-last)))
+ (emms-playlist-previous))
+ (emms-playlist-select (point)))
+ (error
+ (error "No previous track in playlist")))))
+
+(defun emms-playlist-current-select-previous ()
+ "Select the previous track in the current playlist."
+ (with-current-emms-playlist
+ (emms-playlist-select-previous)))
+
+(defun emms-playlist-select-random ()
+ "Select a random track in the current buffer."
+ (emms-playlist-ensure-playlist-buffer)
+ ;; FIXME: This is rather inefficient.
+ (save-excursion
+ (let ((track-indices nil))
+ (goto-char (point-min))
+ (emms-walk-tracks
+ (setq track-indices (cons (point)
+ track-indices)))
+ (setq track-indices (vconcat track-indices))
+ (emms-playlist-select (aref track-indices
+ (random (length track-indices)))))))
+
+(defun emms-playlist-current-select-random ()
+ "Select a random track in the current playlist."
+ (with-current-emms-playlist
+ (emms-playlist-select-random)))
+
+(defun emms-playlist-select-first ()
+ "Select the first track in the current buffer."
+ (emms-playlist-ensure-playlist-buffer)
+ (save-excursion
+ (emms-playlist-first)
+ (emms-playlist-select (point))))
+
+(defun emms-playlist-current-select-first ()
+ "Select the first track in the current playlist."
+ (with-current-emms-playlist
+ (emms-playlist-select-first)))
+
+(defun emms-playlist-select-last ()
+ "Select the last track in the current buffer."
+ (emms-playlist-ensure-playlist-buffer)
+ (save-excursion
+ (emms-playlist-last)
+ (emms-playlist-select (point))))
+
+(defun emms-playlist-current-select-last ()
+ "Select the last track in the current playlist."
+ (with-current-emms-playlist
+ (emms-playlist-select-last)))
+
+;;; Playlist manipulation
+(defun emms-playlist-insert-track (track)
+ "Insert TRACK at the current position into the playlist.
+This uses `emms-playlist-insert-track-function'."
+ (emms-playlist-ensure-playlist-buffer)
+ (funcall emms-playlist-insert-track-function track))
+
+(defun emms-playlist-update-track ()
+ "Update TRACK at point.
+This uses `emms-playlist-update-track-function'."
+ (emms-playlist-ensure-playlist-buffer)
+ (funcall emms-playlist-update-track-function))
+
+(defun emms-playlist-insert-source (source &rest args)
+ "Insert tracks from SOURCE, supplying ARGS as arguments."
+ (emms-playlist-ensure-playlist-buffer)
+ (save-restriction
+ (narrow-to-region (point)
+ (point))
+ (apply source args)
+ (run-hooks 'emms-playlist-source-inserted-hook)))
+
+(defun emms-playlist-current-insert-source (source &rest args)
+ "Insert tracks from SOURCE in the current playlist.
+This is supplying ARGS as arguments to the source."
+ (with-current-emms-playlist
+ (apply 'emms-playlist-insert-source source args)))
+
+(defun emms-playlist-tracks-in-region (beg end)
+ "Return all tracks between BEG and END."
+ (emms-playlist-ensure-playlist-buffer)
+ (let ((tracks nil))
+ (save-restriction
+ (narrow-to-region beg end)
+ (goto-char (point-min))
+ (emms-walk-tracks
+ (setq tracks (cons (emms-playlist-track-at (point))
+ tracks))))
+ tracks))
+
+(defun emms-playlist-track-updated (track)
+ "Update TRACK in all playlist buffers."
+ (mapc (lambda (buf)
+ (with-current-buffer buf
+ (when emms-playlist-buffer-p
+ (save-excursion
+ (let ((pos (text-property-any (point-min) (point-max)
+ 'emms-track track)))
+ (while pos
+ (goto-char pos)
+ (emms-playlist-update-track)
+ (setq pos (text-property-any
+ (next-single-property-change (point)
+ 'emms-track)
+ (point-max)
+ 'emms-track
+ track))))))))
+ (buffer-list))
+ t)
+
+;;; Simple playlist buffer
+(defun emms-playlist-simple-insert-track (track)
+ "Insert the description of TRACK at point."
+ (emms-playlist-ensure-playlist-buffer)
+ (let ((inhibit-read-only t))
+ (insert (emms-propertize (emms-track-force-description track)
+ 'emms-track track)
+ "\n")))
+
+(defun emms-playlist-simple-update-track ()
+ "Update the track at point.
+Since we don't do anything special with the track anyway, just
+ignore this."
+ nil)
+
+(defun emms-playlist-simple-delete-track ()
+ "Delete the track at point."
+ (emms-playlist-ensure-playlist-buffer)
+ (when (not (emms-playlist-track-at (point)))
+ (error "No track at point"))
+ (let ((inhibit-read-only t)
+ (region (emms-property-region (point) 'emms-track)))
+ (delete-region (car region)
+ (cdr region))))
+
+(defun emms-playlist-simple-shuffle ()
+ "Shuffle the whole playlist buffer."
+ (emms-playlist-ensure-playlist-buffer)
+ (let ((inhibit-read-only t)
+ (current nil))
+ (widen)
+ (when emms-player-playing-p
+ (setq current (emms-playlist-selected-track))
+ (goto-char emms-playlist-selected-marker)
+ (emms-playlist-delete-track))
+ (let* ((tracks (vconcat (emms-playlist-tracks-in-region (point-min)
+ (point-max))))
+ (len (length tracks))
+ (i 0))
+ (delete-region (point-min)
+ (point-max))
+ (run-hooks 'emms-playlist-cleared-hook)
+ (emms-shuffle-vector tracks)
+ (when current
+ (emms-playlist-insert-track current))
+ (while (< i len)
+ (emms-playlist-insert-track (aref tracks i))
+ (setq i (1+ i))))
+ (emms-playlist-select-first)
+ (goto-char (point-max))))
+
+(defun emms-playlist-simple-sort ()
+ "Sort the whole playlist buffer."
+ (emms-playlist-ensure-playlist-buffer)
+ (widen)
+ (let ((inhibit-read-only t)
+ (current (emms-playlist-selected-track))
+ (tracks (emms-playlist-tracks-in-region (point-min)
+ (point-max))))
+ (delete-region (point-min)
+ (point-max))
+ (run-hooks 'emms-playlist-cleared-hook)
+ (mapc 'emms-playlist-insert-track
+ (sort tracks emms-sort-lessp-function))
+ (let ((pos (text-property-any (point-min)
+ (point-max)
+ 'emms-track current)))
+ (if pos
+ (emms-playlist-select pos)
+ (emms-playlist-first)))))
+
+(defun emms-uniq-list (list stringify)
+ "Compare stringfied element of list, and remove duplicate elements."
+ ;; This uses a fast append list, keeping a pointer to the last cons
+ ;; cell of the list (TAIL). It might be worthwhile to provide an
+ ;; abstraction for this eventually.
+ (let* ((hash (make-hash-table :test 'equal))
+ (result (cons nil nil))
+ (tail result))
+ (dolist (element list)
+ (let ((str (funcall stringify element)))
+ (when (not (gethash str hash))
+ (setcdr tail (cons element nil))
+ (setq tail (cdr tail)))
+ (puthash str t hash)))
+ (cdr result)))
+
+(defun emms-playlist-simple-uniq ()
+ "Remove duplicate tracks."
+ ;; TODO: This seems unnecessarily destructive.
+ (emms-playlist-ensure-playlist-buffer)
+ (widen)
+ (let ((inhibit-read-only t)
+ (current (emms-playlist-selected-track))
+ (tracks (emms-playlist-tracks-in-region (point-min)
+ (point-max))))
+ (delete-region (point-min) (point-max))
+ (run-hooks 'emms-playlist-cleared-hook)
+ (mapc 'emms-playlist-insert-track
+ (nreverse
+ (emms-uniq-list tracks 'emms-track-name)))
+ (let ((pos (text-property-any (point-min)
+ (point-max)
+ 'emms-track current)))
+ (if pos
+ (emms-playlist-select pos)
+ (emms-playlist-first)))))
+
+(defun emms-default-ok-track-function (track)
+ "A function which OKs all tracks for playing by default."
+ t)
+
+;;; Helper functions
+(defun emms-property-region (pos prop)
+ "Return a pair of the beginning and end of the property PROP at POS.
+If POS does not contain PROP, try to find PROP just before POS."
+ (let (begin end)
+ (if (and (> pos (point-min))
+ (get-text-property (1- pos) prop))
+ (setq begin (previous-single-property-change (1- pos) prop))
+ (if (get-text-property pos prop)
+ (setq begin pos)
+ (error "Cannot find the %s property at the given position" prop)))
+ (if (get-text-property pos prop)
+ (setq end (next-single-property-change pos prop))
+ (if (and (> pos (point-min))
+ (get-text-property (1- pos) prop))
+ (setq end pos)
+ (error "Cannot find the %s property at the given position" prop)))
+ (cons (or begin (point-min))
+ (or end (point-max)))))
+
+(defun emms-shuffle-vector (vector)
+ "Shuffle VECTOR."
+ (let ((i (- (length vector) 1)))
+ (while (>= i 0)
+ (let* ((r (random (1+ i)))
+ (old (aref vector r)))
+ (aset vector r (aref vector i))
+ (aset vector i old))
+ (setq i (- i 1))))
+ vector)
+
+
+;;; Sources
+
+;; A source is just a function which is called in a playlist buffer.
+;; It should use `emms-playlist-insert-track' to insert the tracks it
+;; knows about.
+;;
+;; The define-emms-source macro also defines functions
+;; emms-play-SOURCE and emms-add-SOURCE. The former will replace the
+;; current playlist, while the latter will add to the end.
+
+(defmacro define-emms-source (name arglist &rest body)
+ "Define a new EMMS source called NAME.
+This macro defines three functions: `emms-source-NAME',
+`emms-play-NAME' and `emms-add-NAME'. BODY should use
+`emms-playlist-insert-track' to insert all tracks to be played,
+which is exactly what `emms-source-NAME' will do. The other two
+functions will be simple wrappers around `emms-source-NAME'; any
+`interactive' form that you specify in BODY will end up in these.
+See emms-source-file.el for some examples."
+ (let ((source-name (intern (format "emms-source-%s" name)))
+ (source-play (intern (format "emms-play-%s" name)))
+ (source-add (intern (format "emms-add-%s" name)))
+ (source-insert (intern (format "emms-insert-%s" name)))
+ (docstring "A source of tracks for EMMS.")
+ (interactive nil)
+ (call-args (delete '&rest
+ (delete '&optional
+ arglist))))
+ (when (stringp (car body))
+ (setq docstring (car body)
+ body (cdr body)))
+ (when (eq 'interactive (caar body))
+ (setq interactive (car body)
+ body (cdr body)))
+ `(progn
+ (defun ,source-name ,arglist
+ ,docstring
+ ,@body)
+ (defun ,source-play ,arglist
+ ,docstring
+ ,interactive
+ (if current-prefix-arg
+ (let ((current-prefix-arg nil))
+ (emms-source-add ',source-name ,@call-args))
+ (emms-source-play ',source-name ,@call-args)))
+ (defun ,source-add ,arglist
+ ,docstring
+ ,interactive
+ (if current-prefix-arg
+ (let ((current-prefix-arg nil))
+ (emms-source-play ',source-name ,@call-args))
+ (emms-source-add ',source-name ,@call-args)))
+ (defun ,source-insert ,arglist
+ ,docstring
+ ,interactive
+ (emms-source-insert ',source-name ,@call-args)))))
+
+(defun emms-source-play (source &rest args)
+ "Play the tracks of SOURCE, after first clearing the EMMS playlist."
+ (emms-stop)
+ (emms-playlist-current-clear)
+ (apply 'emms-playlist-current-insert-source source args)
+ (emms-playlist-current-select-first)
+ (emms-start))
+
+(defun emms-source-add (source &rest args)
+ "Add the tracks of SOURCE at the current position in the playlist."
+ (with-current-emms-playlist
+ (save-excursion
+ (goto-char (point-max))
+ (apply 'emms-playlist-current-insert-source source args))
+ (when (or (not emms-playlist-selected-marker)
+ (not (marker-position emms-playlist-selected-marker)))
+ (emms-playlist-select-first))))
+
+(defun emms-source-insert (source &rest args)
+ "Insert the tracks from SOURCE in the current buffer."
+ (if (not emms-playlist-buffer-p)
+ (error "Not in an EMMS playlist buffer")
+ (apply 'emms-playlist-insert-source source args)))
+
+;;; User-defined playlists
+;;; FIXME: Shuffle is bogus here! (because of narrowing)
+(defmacro define-emms-combined-source (name shufflep sources)
+ "Define a `emms-play-X' and `emms-add-X' function for SOURCES."
+ `(define-emms-source ,name ()
+ "An EMMS source for a tracklist."
+ (interactive)
+ (mapc (lambda (source)
+ (apply (car source)
+ (cdr source)))
+ ,sources)
+ ,(when shufflep
+ '(save-restriction
+ (widen)
+ (emms-shuffle)))))
+
+
+;;; Players
+
+;; A player is a data structure created by `emms-player'.
+;; See the docstring of that function for more information.
+
+(defvar emms-player-stopped-p nil
+ "Non-nil if the last EMMS player was stopped by the user.")
+
+(defun emms-player (start stop playablep)
+ "Create a new EMMS player.
+The start function will be START, and the stop function STOP.
+PLAYABLEP should return non-nil for tracks that this player can
+play.
+
+When trying to play a track, EMMS walks through
+`emms-player-list'. For each player, it calls the PLAYABLEP
+function. The player corresponding to the first PLAYABLEP
+function that returns non-nil is used to play the track. To
+actually play the track, EMMS calls the START function, passing
+the chosen track as a parameter.
+
+If the user tells EMMS to stop playing, the STOP function is
+called. Once the player has finished playing, it should call
+`emms-player-stopped' to let EMMS know."
+ (let ((p (emms-dictionary '*player*)))
+ (emms-player-set p 'start start)
+ (emms-player-set p 'stop stop)
+ (emms-player-set p 'playablep playablep)
+ p))
+
+(defun emms-player-get (player name &optional inexistent)
+ "Return the value of entry NAME in PLAYER."
+ (let ((p (if (symbolp player)
+ (symbol-value player)
+ player)))
+ (emms-dictionary-get p name inexistent)))
+
+(defun emms-player-set (player name value)
+ "Set the value of entry NAME in PLAYER to VALUE."
+ (let ((p (if (symbolp player)
+ (symbol-value player)
+ player)))
+ (emms-dictionary-set p name value)))
+
+(defun emms-player-for (track)
+ "Return an EMMS player capable of playing TRACK.
+This will be the first player whose PLAYABLEP function returns
+non-nil, or nil if no such player exists."
+ (let ((lis emms-player-list))
+ (while (and lis
+ (not (funcall (emms-player-get (car lis) 'playablep)
+ track)))
+ (setq lis (cdr lis)))
+ (if lis
+ (car lis)
+ nil)))
+
+(defun emms-player-start (track)
+ "Start playing TRACK."
+ (if emms-player-playing-p
+ (error "A player is already playing")
+ (let ((player (emms-player-for track)))
+ (if (not player)
+ (error "Don't know how to play track: %S" track)
+ ;; Change default-directory so we don't accidentally block any
+ ;; directories the current buffer was visiting.
+ (let ((default-directory "/"))
+ (funcall (emms-player-get player 'start)
+ track))))))
+
+(defun emms-player-started (player)
+ "Declare that the given EMMS PLAYER has started.
+This should only be done by the current player itself."
+ (setq emms-player-playing-p player
+ emms-player-paused-p nil)
+ (run-hooks 'emms-player-started-hook))
+
+(defun emms-player-stop ()
+ "Stop the current EMMS player."
+ (when emms-player-playing-p
+ (let ((emms-player-stopped-p t))
+ (funcall (emms-player-get emms-player-playing-p 'stop)))
+ (setq emms-player-playing-p nil)))
+
+(defun emms-player-stopped ()
+ "Declare that the current EMMS player is finished.
+This should only be done by the current player itself."
+ (setq emms-player-playing-p nil)
+ (if emms-player-stopped-p
+ (run-hooks 'emms-player-stopped-hook)
+ (sleep-for emms-player-delay)
+ (run-hooks 'emms-player-finished-hook)
+ (funcall emms-player-next-function)))
+
+(defun emms-player-pause ()
+ "Pause the current EMMS player."
+ (cond
+ ((not emms-player-playing-p)
+ (error "Can't pause player, nothing is playing"))
+ (emms-player-paused-p
+ (let ((resume (emms-player-get emms-player-playing-p 'resume))
+ (pause (emms-player-get emms-player-playing-p 'pause)))
+ (cond
+ (resume
+ (funcall resume))
+ (pause
+ (funcall pause))
+ (t
+ (error "Player does not know how to pause"))))
+ (setq emms-player-paused-p nil)
+ (run-hooks 'emms-player-paused-hook))
+ (t
+ (let ((pause (emms-player-get emms-player-playing-p 'pause)))
+ (if pause
+ (funcall pause)
+ (error "Player does not know how to pause")))
+ (setq emms-player-paused-p t)
+ (run-hooks 'emms-player-paused-hook))))
+
+(defun emms-player-seek (seconds)
+ "Seek the current player by SECONDS seconds.
+This can be a floating point number for fractions of a second, or
+negative to seek backwards."
+ (if (not emms-player-playing-p)
+ (error "Can't seek player, nothing playing right now")
+ (let ((seek (emms-player-get emms-player-playing-p 'seek)))
+ (if (not seek)
+ (error "Player does not know how to seek")
+ (funcall seek seconds)
+ (run-hook-with-args 'emms-player-seeked-functions seconds)))))
+
+(defun emms-player-seek-to (seconds)
+ "Seek the current player to SECONDS seconds.
+This can be a floating point number for fractions of a second, or
+negative to seek backwards."
+ (if (not emms-player-playing-p)
+ (error "Can't seek-to player, nothing playing right now")
+ (let ((seek (emms-player-get emms-player-playing-p 'seek-to)))
+ (if (not seek)
+ (error "Player does not know how to seek-to")
+ (funcall seek seconds)
+ (run-hook-with-args 'emms-player-time-set-functions seconds)))))
+
+(provide 'emms)
+;;; emms.el ends here