;;; emms-tageditor.el --- Info-editor for EMMS ;; Copyright (C) 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 file provides an info-editor for EMMS. It also provides, as an ;; option, functions for integrating this with the ;; playlist-buffer-interface as well as the mark-module for the pbi. ;; To activate the basic editor, use this in your EMMS-configuration: ;; (require 'emms-tageditor) ;; And call M-x emms-tageditor-edit-current RET, when you find a song ;; with a tag that needs to be changed. ;; To use the pbi-functionality, add the following to your ;; configuration: ;; (emms-tageditor-pbi-mode 1) ;; Which will add 'e' as a key, to edit the track under the point in ;; the pbi. ;; To use the extended pbi-functionality, with `emms-pbi-mark', use: ;; (emms-tageditor-pbi-mark-mode 1) ;; Which likewise will bind 'E' to edit all the tracks marked as a ;; whole, in a way that allows you to set the "album"-tag of each ;; track, or f.ex. to match the filename to a regexp, and set a group ;; from that regexp as the title-tag, or any other. ;;; Code: (require 'emms) (require 'emms-info) (require 'emms-pbi) (require 'emms-pbi-mark) (require 'widget) (require 'wid-edit) ;; Custom (defgroup emms-tageditor nil "*A wizard-style tageditor for EMMS." :group 'emms :prefix "emms-tageditor-") (defcustom emms-tageditor-buffer-name "*emms-tageditor*" "The name of the buffer used for tag-editing." :type 'string :group 'emms-tageditor) ;; Variables (defvar emms-tageditor-current-tracks [] "A vector of the tracks currently being edited.") (defvar emms-tageditor-current-infos [] "A vector of the info-ojects currently being edited.") (defvar emms-tageditor-message nil "A message to show with the form, if non-nil.") (defvar emms-tageditor-widgets nil "A hash map of the widgets on the screen.") ;; Hashtable interface (defun emms-tageditor-get-widget (trackidx fieldname) "Return the widget of FIELDNAME, for the track with TRACKIDX" (let ((symbol (emms-tageditor-get-widget-id trackidx fieldname))) (gethash symbol emms-tageditor-widgets))) (defun emms-tageditor-set-widget (trackidx fieldname widget) "Save a reference to WIDGET, as FIELDNAME of the track with TRACKIDX." (let ((symbol (emms-tageditor-get-widget-id trackidx fieldname))) (puthash symbol widget emms-tageditor-widgets))) ;; Helper function for the hashtable (defun emms-tageditor-get-widget-id (trackidx fieldname) "Get a symbol ID of the FIELDNAME widget of TRACKIDX." (intern (concat "-trackidx-" (number-to-string trackidx) "-" (symbol-name fieldname)))) (defun emms-tageditor-read-tag (trackidx) "Read the form for a single track, and parse it into an info-object." (let ((info (aref emms-tageditor-current-infos trackidx)) (track (aref emms-tageditor-current-tracks trackidx)) (tags '(title artist album note))) (while tags (let* ((tag (car tags)) (infotag (intern (concat "emms-info-" (symbol-name tag))))) (eval `(setf (,infotag ,info) ,(widget-value (emms-tageditor-get-widget trackidx tag))))) (setq tags (cdr tags))) info)) ;; Parsing the form (defun emms-tageditor-read-tags (tracks) "Create a new list of info-object from the form, and return it." (mapcar 'emms-tageditor-read-tag tracks)) ;; Event-handling (defun emms-tageditor-save (widget &rest ignore) "Save the info of a single tag." ;; This function only saves a single form. Figure out which track ;; this is bound to, by extracting the trackidx from the (let* ((trackidx (widget-get widget :trackidx)) (track (aref emms-tageditor-current-tracks trackidx))) ;; Let's make sure we have an info-source capable of writing tags. (if (funcall (emms-info-method-for track) 'set) (progn ;; we have an info-method for it, let's set it. (emms-info-set track (emms-tageditor-read-tag trackidx)) (when (and (featurep 'emms-pbi) (get-buffer emms-pbi-playlist-buffer-name)) ;; If a playlist is available, it's info might need to be updated ;; for this track. (emms-pbi-entry-update-track track) (when (= (length emms-tageditor-current-tracks) 1) ;; pressing save should kill the buffer when only one track is ;; being edited. (emms-tageditor-cleanup)))) ;; if the above returned nil, no function to save info for this ;; track has been made! signal an error and escape! (message (format (concat "Track %s doesn't have an associated info-method " " capable of saving data") (emms-track-name track)))))) (defun emms-tageditor-save-all () "Save all entries currently being edited." ;; Loop through all forms, and save them (let ((idx 0)) (while (< idx (length emms-tageditor-current-tracks)) (emms-tageditor-save (emms-tageditor-get-widget idx 'save)) (setq idx (1+ idx)))) ;; Always cleanup when saving everything (emms-tageditor-cleanup)) (defun emms-tageditor-cancel (&rest ignore) (emms-tageditor-cleanup)) (defun emms-tageditor-create-string (rep times) "Concat TIMES occurances of REP into a string and return it." (if (> times 1) (concat (emms-tageditor-create-string rep (1- times)) rep) rep)) ;; Setting up the form, and destroying it (defun emms-tageditor-create-widgets (trackidx info) "Create widgets for a single track-form" (let ((inhibit-read-only t) (track (aref emms-tageditor-current-tracks trackidx)) (info (aref emms-tageditor-current-infos trackidx))) (goto-char (point-min)) (widget-insert (format "Editing tag for track: %s (%s)\n" (emms-track-name track) (symbol-name (emms-track-type track)))) (widget-insert (concat "----------------------------------------" "----------------------------------------" "\n")) ;; Insert the tags (let ((tags '(title artist album note))) (while tags (let* ((tag (car tags)) (info-tag (intern (concat "emms-info-" (symbol-name tag)))) (tag-name (concat (upcase-initials (symbol-name tag)) ":" (emms-tageditor-create-string " " (- 10 (1+ (length (symbol-name tag)))))))) (widget-insert tag-name) (eval `(emms-tageditor-set-widget trackidx (quote ,tag) (widget-create 'editable-field :size 69 :trackidx trackidx :value (,info-tag ,info)))) (when (not (= (length tags) 1)) (widget-insert "\n"))) (setq tags (cdr tags)))) ;; Insert the rest (widget-insert (concat "\n" "----------------------------------------" "----------------------------------------" "\n")) (emms-tageditor-set-widget trackidx 'save (widget-create 'push-button :notify 'emms-tageditor-save :trackidx trackidx :help-echo "Save changes to this tag" "Save")) (widget-insert " ") (widget-create 'push-button :notify 'emms-tageditor-cancel :help-echo "Cancel changes" "Cancel"))) (defun emms-tageditor-cleanup () "Clean up and exit the tageditor." ;; delete all widgets (let ((idx 0)) (while (< idx (length emms-tageditor-current-tracks)) (when emms-tageditor-widgets (let ((tags '(title artist album note save))) (while tags (let ((tag (car tags))) (eval `(widget-delete (emms-tageditor-get-widget idx tag)))) (setq tags (cdr tags))))) ;; continue idx loop (setq idx (1+ idx)))) ;; kill the buffer & delete the hashmap (setq emms-tageditor-widgets nil) (kill-buffer (get-buffer-create emms-tageditor-buffer-name))) (defun emms-tageditor-replace-create-replacement (replace-with trackidx) (let ((info (aref emms-tageditor-current-infos trackidx)) (track (aref emms-tageditor-current-tracks trackidx))) (setq replace-with (emms-replace-regexp-in-string "$TITLE" (emms-info-title info) replace-with)) (setq replace-with (emms-replace-regexp-in-string "$ALBUM" (emms-info-album info) replace-with)) (setq replace-with (emms-replace-regexp-in-string "$ARTIST" (emms-info-artist info) replace-with)) (setq replace-with (emms-replace-regexp-in-string "$NOTE" (emms-info-note info) replace-with)) (setq replace-with (emms-replace-regexp-in-string "$TRACKNAME" (emms-track-name track) replace-with))) replace-with) (defun emms-tageditor-replace-tag (field regexp replace-with) "Replace REGEXP with REPLACE-WITH in all fields of type FIELD." (let ((idx 0)) (while (< idx (length emms-tageditor-current-tracks)) ;; Find the widget for the current track (let ((widget (emms-tageditor-get-widget idx field))) (let* ((str (widget-value widget)) (str (emms-replace-regexp-in-string regexp replace-with str))) (if (string= "$SET" regexp) (widget-value-set widget (emms-tageditor-replace-create-replacement replace-with idx)) (widget-value-set widget (emms-tageditor-replace-create-replacement str idx))))) (setq idx (1+ idx))))) (defun emms-tageditor-replace-tags (&optional field regexp replace-with) "Replace REGEXP with REPLACE-WITH in the widgets matching FIELD." (interactive) (setq field (or field (intern (completing-read "Select which tags to replace in: " '(("all" . all) ("title" . title) ("artist" . artist) ("album" . album) ("note" . note)) nil t "title")))) (setq regexp (or regexp (read-from-minibuffer "Regexp to replace: "))) (setq replace-with (or replace-with (read-from-minibuffer (concat "Replace regexp " regexp " with: ")))) ;; Having all input, let's continue to act on it. (when (and field regexp replace-with) ;; two cases, 'all or something else (if (equal field 'all) (progn ;; We need a sweep-search of all tag-fields (let ((tags '(title artist album note))) (while tags (emms-tageditor-replace-tag (car tags) regexp replace-with) (setq tags (cdr tags))))) ;; only search the field called field (emms-tageditor-replace-tag field regexp replace-with)) ;; we've probably changed some widget values, so we need to make ;; them count. (widget-setup))) ;; Setting up the buffer (defun emms-tageditor-edit (tracks &optional infos) "Open an editor for the vector TRACKS. Optionally, use the vector INFOS as the default info for each track, and use the function SAVEFUNCTION as the event-handler for each save-button." ;; Save variables (setq emms-tageditor-current-tracks tracks) (if infos (setq emms-tageditor-current-infos infos) ;; Otherwise, create the vector of infos by loading them. (setq emms-tageditor-current-infos (make-vector (length emms-tageditor-current-tracks) nil)) (let ((idx 0)) (while (< idx (length emms-tageditor-current-tracks)) (setf (aref emms-tageditor-current-infos idx) ;; should we allow cache here? (emms-info-get (aref emms-tageditor-current-tracks idx))) (setq idx (1+ idx))))) ;; Kill the buffer, then recreate it. Otherwise, everything will be ;; in one big widget. (kill-buffer (get-buffer-create emms-tageditor-buffer-name)) (switch-to-buffer (get-buffer-create emms-tageditor-buffer-name)) ;; Initialise buffer (kill-all-local-variables) (widget-minor-mode 1) ;; Setup widget hashmap, (setq emms-tageditor-widgets (make-hash-table :test 'equal)) ;; and create the widgets (let ((idx 0)) (while (< idx (length emms-tageditor-current-tracks)) (emms-tageditor-create-widgets idx (aref emms-tageditor-current-infos idx)) (widget-insert "\n\n") (setq idx (1+ idx))) ;; Create the save _all_ widget? ;; setup the help-message (when emms-tageditor-message (goto-char (point-max)) (widget-insert (concat "\n" "........................................" "........................................" "\n")) (widget-insert emms-tageditor-message) (setq emms-tageditor-message nil)) (use-local-map widget-keymap) ;; Bind some additional keys (widget-setup) (local-set-key (kbd "C-x C-s") (lambda () (interactive) (emms-tageditor-save-all))) (local-set-key (kbd "C-c C-r") 'emms-tageditor-replace-tags) (local-set-key (kbd "ESC") (lambda () (interactive) (emms-tageditor-cancel))))) ;; Entry function (defun emms-tageditor-edit-current () "Edit the info of the currently playing track" (interactive) (emms-tageditor-edit (vconcat (list (emms-playlist-current-track))))) ;; Integrating with emms-pbi (defvar emms-tageditor-pbi-mark-message "When editing multiple files, some things works a bit differently. First of all, to save *all* changes made to tracks, use C-x C-s. The changes to each individual track, can be saved by using the corresponding Save-buttons. To utilize the full power of this mode of editing, you should use M-x emms-tageditor-replace-tags RET, bound to C-c C-r. When using `emms-tageditor-replace-tags', you have the following special keyword available: For matching: $SET -- Attempting to replace this value with anything, will tell the function to simply override the previous value. For what to replace with: $TRACKNAME -- The trackname (for file-type tracks, the full filename) $TITLE -- The (saved) title of this track. $ARTIST -- Likewise, with the artist $ALBUM -- Likewise, with the album $ALBUM -- Likewise, with the note. NOT IMPLEMENTED YET: If the power of that function doesn't fit your needs, you can use M-x emms-tageditor-toggle-read-only RET, bound to C-c C-t. This function will make the buffer read-only, which means you can use the regular editing functions on the entire buffer. This means that doing an M-x replace-regexp RET, won't halt if it matches any of the text outsie widgets, as it would otherwise.") (defun emms-tageditor-pbi-mode (&optional arg) "Register the intergration with the playlist-buffer interface for EMMS. Turn the registration on, if and only if ARG is a positive integer, off otherwise." (interactive "p") (if (not (featurep 'emms-pbi)) (message "You need `emms-pbi' loaded to use this!") (if (and (numberp arg) (< 0 arg)) (add-hook 'emms-pbi-mode-hook 'emms-tageditor-pbi-register) (remove-hook 'emms-pbi-mode-hook 'emms-tageditor-pbi-register)))) (defun emms-tageditor-pbi-register () "Register keybindings for the playlist-buffer interface. Should be run in `emms-pbi-mode-hook'." (local-set-key (kbd "e") 'emms-tageditor-pbi-edit-current-line)) (defun emms-tageditor-pbi-edit-current-line () "Edit the track under point." (interactive) (if (not (featurep 'emms-pbi)) (message "You need `emms-pbi' loaded to use this!") ;; Fetch track under current line (let ((curidx (emms-pbi-return-current-line-index))) (when (emms-pbi-valid-index-p curidx) (other-window 1) (emms-tageditor-edit (vconcat (list (emms-playlist-get-track curidx)))))))) ;; Integrating with pbi-mark (defun emms-tageditor-pbi-mark-mode (&optional arg) "Register the intergration with the playlist-buffer-marks for EMMS. Turn the integration on, if and only if ARG is a positive integer, off otherwise." (interactive "p") (if (not (featurep 'emms-pbi-mark)) (message "You need `emms-pbi-mark' loaded to use this!") (if (and (numberp arg) (< 0 arg)) (add-hook 'emms-pbi-mode-hook 'emms-tageditor-pbi-mark-register) (remove-hook 'emms-pbi-mode-hook 'emms-tageditor-pbi-mark-register)))) (defun emms-tageditor-pbi-mark-register () "Register keybindings for the playlist-buffer interface marking functions. Should be run in `emms-pbi-mode-hook'." (local-set-key (kbd "E") 'emms-tageditor-pbi-mark-edit-marked-entries)) (defun emms-tageditor-pbi-mark-edit-marked-entries () "Edit all marked entries as one, using a special editor." (interactive) (if (not (featurep 'emms-pbi-mark)) (message "You need `emms-pbi-mark' loaded to use this!") (other-window 1) (setq emms-tageditor-message emms-tageditor-pbi-mark-message) (emms-tageditor-edit (vconcat (emms-pbi-mark-get-marked))) (goto-char (point-min)))) (provide 'emms-tageditor) ;;; emms-tageditor.el ends here