diff options
Diffstat (limited to 'emms.el')
-rw-r--r-- | emms.el | 1528 |
1 files changed, 1518 insertions, 10 deletions
@@ -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 |