;;; pbi.el --- Playlist-buffer interface for emms.el ;; Copyright (C) 2003, 2004, 2005, 2006 Free Software Foundation, Inc. ;; Author: Ulrik Jensen ;; Keywords: ;; This file 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 2, or (at your option) ;; any later version. ;; This file 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 GNU Emacs; see the file COPYING. If not, write to the ;; Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, ;; Boston, MA 02110-1301 USA ;;; Commentary: ;; This module provices a playlist-buffer interface (pbi) to EMMS. ;; To use it, just add the following to your emms-configuration ;; (.emacs, for example): ;; (require 'emms-pbi) ;; If you want the playlist to be generated automagically when you ;; start playing your music, use the following: ;; (emms-pbi 1) ;; Another common usage, is to just load the playlist when you need ;; it. This can be achieved by starting emms, and then typing M-x ;; emms-pbi RET. ;; The default look of the playlist depends on the value of ;; `emms-track-description-function'. This function takes a ;; track-value and returns a string, that will then be formatted ;; further, and inserted into the playlist-buffer. If you want a ;; different function to handle the base-description, but you only ;; want that function for emms-pbi, you can override ;; `emms-pbi-track-description-function', which will fall back to ;; `emms-track-description-function' when nil ;; You'll probably also want to customize the faces, in which case, ;; you do M-x customize-group RET emms-pbi RET and tweak. ;; To get id3-tags, and ogg-info, you should look at ;; emms-info.el. This file provides amongst other things, functions ;; suitable as values of `emms-track-description-function' (or if you ;; only want the info in emms-pbi, values of ;; `emms-pbi-track-description-function'). ;; If you use a big playlist, and info, you probably don't want info ;; to load everything right at once. This can be accomplished through ;; the `emms-info-later-do'-module, which will gradually load the ;; playlist. A note on how to make it work with the PBI as well, is ;; included in that file (and also a part of emms-default.el, ;; currently the 'cvs-setup) ;; Linenumbering (Changed!) ;; ;; Prior versions of emms-pbi had their own linenumbering ;; functions. But these functions were either error prone or damn ;; slow. And besides there was already a emacs mode that does exactly ;; the same: setnu.el So we remove the linenumbering functions in ;; favour of setnu. You can get setnu from ;; http://www.wonderworks.com/download/setnu.el. To get linenumbers ;; just put the following code in your ~/.emacs and put setnu.el ;; somewhere on your loadpath: ;; ;; (require 'setnu) ;; (add-hook 'emms-pbi-after-build-hook (lambda () (setnu-mode 1))) ;;; Code: (require 'emms) (require 'cl) (defvar emms-pbi-version "0.2 $Revision: 1.37 $" "EMMS pbi version string.") ;; $Id: emms-pbi.el,v 1.37 2005/07/09 19:14:57 yonirabkin Exp $ ;; Customizations (defgroup emms-pbi nil "*A playlist-buffer user-interface for EMMS." :group 'emms :prefix "emms-pbi-") (defcustom emms-pbi-playlist-entry-generate-function 'identity "*The function to call for generating a single item of the playlist. This will be called with a string argument FILENAME, and should return the text to be inserted in the playlist." :type 'function :group 'emms-pbi) (defcustom emms-pbi-playlist-entry-max-length nil "*The maximum length of an entry in the playlist. If this is nil, the entire string provided by `emms-track-description-function'. will be used. Beware, the output of that function is cut off to fit the max-length before running `emms-pbi-playlist-entry-generate-function'." :type '(restricted-sexp :match-alternatives (integerp 'nil)) :group 'emms-pbi) (defcustom emms-pbi-playlist-buffer-name "*Playlist*" "Name of the buffer to use as a playlist-buffer" :type 'string :group 'emms-pbi) (defcustom emms-pbi-track-description-function nil "Returns a description for the playlist. Take track as only argument. If `emms-pbi-track-description-function' is nil, `emms-track-description-function' is used instead." :type 'function :group 'emms-pbi) ;; Hooks (defcustom emms-pbi-after-build-hook nil "Hook that is run after the playlist buffer is built. That might be usefull to change the playlist buffer before the buffer is set read-only." :type 'hook :group 'emms-pbi) (defcustom emms-pbi-current-line-face-changed-hook nil "Hook that is called when the face of the current line changes." :type 'hook :group 'emms-pbi) (defcustom emms-pbi-manually-change-song-hook nil "Hook that is called when the song is manually changed." :type 'hook :group 'emms-pbi) ;; Faces (defface emms-pbi-song-face '((((class color) (background light)) (:foreground "red")) (((class color) (background dark)) (:foreground "red"))) "Face used for songs" :group 'emms-pbi) (defface emms-pbi-current-face '((((class color)(background light)) (:foreground "blue" :weight bold)) (((class color)(background dark)) (:foreground "yellow" :weight bold))) "Face used for the currently played song" :group 'emms-pbi) ;; Variables (defvar emms-pbi-suspend-hooks nil "When this variable is t, the hooks updating the playlist stop reacting. If you do something with `emms-playlist', and don't want to regenerate the entire playlist-buffer, a good idea is to bind `emms-pbi-suspend-hooks' to t when you set `emms-playlist'.") (defvar emms-pbi-current-overlay nil "Overlay moving with the current track.") (defvar emms-kill-ring () "Kill-ring for the playlist buffer.") (defvar emms-pbi-longest-line 0 "The length of the longest line yet inserted.") ;; Entry points (defun emms-pbi (arg) "Turn on emms-playlist if prefix argument ARG is a positive integer, off otherwise." (interactive "p") (if (and arg (> arg 0)) (progn (add-hook 'emms-player-started-hook 'emms-pbi-update-current-face) ;; make sure this is appended! (add-hook 'emms-playlist-changed-hook 'emms-pbi-rebuild-playlist-buffer t) ;; build the playlist, if we have a playlist (if (> (length emms-playlist) 0) (progn (emms-pbi-build-playlist-buffer) (switch-to-buffer emms-pbi-playlist-buffer-name)) (message "Empty playlist, won't build playlist-buffer!"))) (remove-hook 'emms-player-stopped-hook 'emms-pbi-remove-current-face) (remove-hook 'emms-player-starter-hook 'emms-pbi-add-current-face) (remove-hook 'emms-playlist-changed-hook 'emms-pbi-rebuild-playlist-buffer))) (defun emms-pbi-shorten-entry-to-max-length (entry) "Cut off an entry-text to make sure it's no longer than `emms-pbi-playlist-entry-max-length' characters long." (if (and emms-pbi-playlist-entry-max-length (> (length entry) emms-pbi-playlist-entry-max-length)) (substring entry 0 emms-pbi-playlist-entry-max-length) entry)) (defun emms-position-vector (elt vector) "Returns the index of elt in vector" (let ((curidx 0) (residx nil)) (while (and (< curidx (length vector)) (eq residx nil)) (let ((curelt (aref vector curidx))) (when (equal elt curelt) (setq residx curidx))) (setq curidx (1+ curidx))) residx)) ;; This function should probably be phased out, since it depends too ;; much on emms-info. All uses should be replaced by the function ;; below: (defun emms-pbi-entry-info-updated (track info) "Update the track-entry based on the info" (save-excursion (set-buffer emms-pbi-playlist-buffer-name) (let ((inhibit-read-only t) (pos (emms-position-vector track emms-playlist))) ;; find the entry in the playlist, corresponding to TRACK (when (goto-line (1+ pos)) ;; update the text of it - by generating it again simply ;; first, find the index of the entry in the playlist. ;; and save the current properties (delete-region (point-at-bol) (point-at-eol)) (emms-pbi-insert-entry (emms-playlist-get-track pos)) ;; and update them (emms-pbi-add-properties-current-line))))) (defun emms-pbi-entry-update-track (track) "Update the playlist entry for TRACK." (let ((trackidx (loop for i from 0 for a across (emms-playlist-get-playlist) if (equal a track) return i))) (when (and trackidx (emms-pbi-valid-index-p trackidx)) (emms-pbi-entry-update-idx trackidx)))) (defun emms-pbi-entry-update-idx (trackidx) "Update the playlist entry for the track at index TRACKIDX." (save-excursion (set-buffer emms-pbi-playlist-buffer-name) ;; Find the track (when (emms-pbi-valid-index-p trackidx) (let ((lineidx (1+ trackidx))) (let ((inhibit-read-only t)) ;; Erase the line (goto-line lineidx) (delete-region (point-at-bol) (point-at-eol)) ;; Insert the track and add properties (emms-pbi-insert-entry (emms-playlist-get-track trackidx)) (emms-pbi-add-properties-current-line) ;; Make sure the overlay is in place (emms-pbi-update-current-face)))))) (defun emms-pbi-entry-generate (track) "Generate an entry for TRACK in the playlist-buffer. This uses `emms-pbi-track-description-function', or if that is nil, it defaults to `emms-track-description'." (if emms-pbi-track-description-function (funcall emms-pbi-track-description-function track) ;; default to the emms way (funcall emms-track-description-function track))) (defun emms-pbi-insert-entry (track) "Insert an entry for TRACK in the playlist." (let ((inhibit-read-only t) (line (emms-pbi-shorten-entry-to-max-length (emms-pbi-entry-generate track)))) (insert line) (emms-pbi-add-properties-current-line) ;; for the convenience of other modules, keep track of the longest ;; line yet. (setq emms-pbi-longest-line (max emms-pbi-longest-line (length line))))) (defun emms-pbi-rebuild-playlist-buffer () "This function rebuilds the playlist-buffer if necessary." (unless emms-pbi-suspend-hooks (emms-pbi-build-playlist-buffer))) ;; Function for building the playlist (defun emms-pbi-build-playlist-buffer () "Build a playlist-buffer based on the current playlist." (save-excursion (set-buffer (get-buffer-create emms-pbi-playlist-buffer-name)) (let ((playlist-length (length emms-playlist)) (idx 0) (inhibit-read-only t)) ;; reset the buffer (erase-buffer) ;; insert all elements (while (< idx playlist-length) (emms-pbi-insert-entry (emms-playlist-get-track idx)) (insert "\n") (setq idx (1+ idx))) ;; Initialise the buffer variables ;; remove the last line (emms-pbi-update-current-face) (delete-backward-char 1) (run-hooks 'emms-pbi-after-build-hook) (setq buffer-read-only t) (emms-pbi-mode) ;; as the last thing we do, update the current-face. (when emms-player-playing-p (emms-pbi-update-current-face))))) ;; Updating the currently playing face (defun emms-pbi-update-current-face () "Updates the file line with the current-face" (when (get-buffer emms-pbi-playlist-buffer-name) (save-excursion (set-buffer emms-pbi-playlist-buffer-name) (let ((inhibit-read-only t)) ;; don't try to `1+' the value `nil' (unless (null emms-playlist-current) (goto-line (1+ emms-playlist-current)) (if (overlayp emms-pbi-current-overlay) (move-overlay emms-pbi-current-overlay (point-at-bol) (point-at-eol)) (setq emms-pbi-current-overlay (make-overlay (point-at-bol) (point-at-eol))) (overlay-put emms-pbi-current-overlay 'face 'emms-pbi-current-face))))))) ;;Handling faces & properties (defun emms-pbi-add-properties-current-line () "Adds the correct faces and other properties to the current line" ;; Default face (let ((idx (emms-pbi-return-current-line-index))) (add-text-properties (point-at-bol) (point-at-eol) '(face emms-pbi-song-face)) (run-hooks 'emms-pbi-current-line-face-changed-hook))) (defun emms-pbi-play-current-line () "Play the current line" (interactive) (let ((new-idx (emms-pbi-return-current-line-index))) ;; check boundaries (when (and new-idx (> new-idx -1) (< new-idx (length emms-playlist))) (emms-stop) (emms-playlist-set-current new-idx) (emms-start) (run-hooks 'emms-pbi-manually-change-song-hook)))) (defun emms-pbi-show-current-line () "Show filename and info for track on current line." (interactive) (let ((idx (emms-pbi-return-current-line-index))) (message "Filename: %s; Info: %s" (emms-track-name (emms-playlist-get-track idx)) (emms-playlist-get idx)))) ;; Major-mode for the playlist-buffer (define-derived-mode emms-pbi-mode fundamental-mode "EMMS playlist" (suppress-keymap emms-pbi-mode-map t) (define-key emms-pbi-mode-map (kbd "n") 'emms-next) (define-key emms-pbi-mode-map (kbd "p") 'emms-previous) (define-key emms-pbi-mode-map (kbd "c") 'emms-pbi-recenter) (define-key emms-pbi-mode-map (kbd "l") 'emms-pbi-recenter) (define-key emms-pbi-mode-map (kbd "C-x C-s") 'emms-pbi-export-playlist) (define-key emms-pbi-mode-map (kbd "C-k") 'emms-pbi-kill-line) (define-key emms-pbi-mode-map (kbd "d") 'emms-pbi-kill-line) (define-key emms-pbi-mode-map (kbd "C-y") 'emms-pbi-yank) (define-key emms-pbi-mode-map (kbd "s") 'emms-stop) (define-key emms-pbi-mode-map (kbd "f") 'emms-pbi-show-current-line) (define-key emms-pbi-mode-map (kbd "RET") 'emms-pbi-play-current-line) (define-key emms-pbi-mode-map (kbd "q") 'bury-buffer) (define-key emms-pbi-mode-map (kbd "") 'emms-pbi-play-current-line) (define-key emms-pbi-mode-map (kbd "Q") 'emms-pbi-quit) (define-key emms-pbi-mode-map (kbd "?") 'describe-mode)) (defun emms-pbi-quit () "Stops emms and kill the playlist buffer" (interactive) (emms-stop) (kill-buffer emms-pbi-playlist-buffer-name)) (defun emms-playlist-empty-p () (= (length emms-playlist) 0)) (defun emms-pbi-kill-line () "Kill the playlist line the cursor is currently on and update the playlist accordingly." (interactive) (if (emms-playlist-empty-p) (message "One cannot remove what is not there grasshopper") (let ((idx (emms-pbi-return-current-line-index)) (inhibit-read-only t)) ;; remove from buffer (goto-char (point-at-bol)) (kill-line 1) ;; push track onto emms-kill-ring (push (aref emms-playlist idx) emms-kill-ring) ;; now delete the entry from the playlist. - making sure that ;; the entire list isn't regenerated (let ((emms-pbi-suspend-hooks t)) (emms-playlist-remove idx)) (if (numberp emms-playlist-current) ;; this deals with edge cases, explicit (cond ((and emms-player-playing-p (= idx emms-playlist-current)) (emms-stop) (emms-start) (emms-pbi-update-current-face)) ((= idx emms-playlist-current) (emms-pbi-update-current-face)) ((< idx emms-playlist-current) (setq emms-playlist-current (1- emms-playlist-current)))) (emms-stop))))) ;; stop if playlist empty after kill (defun emms-pbi-yank () "Yank a filename from `kill-ring' into the playlist." (interactive) (let ((track (pop emms-kill-ring)) (inhibit-read-only t) (idx (emms-pbi-return-current-line-index))) (if (and track (emms-player-for track)) ;; only insert files that actually exist, and can be played. ;; insert it into the buffer (save-excursion (goto-char (point-at-bol)) (insert "\n") (forward-line -1) (goto-char (point-at-bol)) (emms-pbi-insert-entry track) ;; insert it into the playlist (let ((emms-pbi-suspend-hooks t)) (emms-playlist-add (list track) idx)))) (message "No playable track in emms-kill-ring!"))) (defun emms-pbi-return-current-line-index () "Return the index position in the playlist of the current line." (1- (count-lines (point-min) (point-at-eol)))) (defun emms-pbi-valid-index-p (idx) "Return non-nil if IDX is a valid index in the current playlist." (and idx (> idx -1) (< idx (length emms-playlist)))) (defun emms-pbi-recenter () "Center on current playing track" (interactive) (let ((line (1+ emms-playlist-current))) (goto-line line) (recenter))) (defun emms-pbi-export-playlist (file) (interactive "Fsave playlist:") (let ((buffer (find-file-noselect file t))) (set-buffer buffer) (prin1 emms-playlist buffer) (save-buffer) (kill-buffer buffer))) (defun emms-pbi-open-playlist (file) (interactive "fOpen playlist:") (let ((buffer (find-file-noselect file))) (set-buffer buffer) (beginning-of-buffer) (emms-playlist-set-playlist (read buffer)) (kill-buffer buffer))) (provide 'emms-pbi) ;;; emms-pbi.el ends here