;;; ;;; Mode for editing CDDB files (i.e. xmcd CD database files) ;;; ;;; Created: Waider / 05/10/2000 ;;; Last Modified: Waider / 09/06/2001 ;;; ;;; Things to fix: ;;; 1. Long lines can be wrapped by putting in a second ;;; TTITLE\d+=... line, but this code won't handle that. ;;; 2. Enforce line lengths and character ranges ;;; 3. Autocomplete stuff fails badly when you're playing with non-ASCII. ;; is this strictly necessary? (defvar cddb-mode 'text-mode "CDDB mode") ;;; ;;; AutoComplete support ;;; (defun cddb-keymap-maybe-kill() "smarter version of `self-insert-command'. If the character under the cursor is the same as the key pressed, move forward over it. Otherwise, delete the specially-marked region, unset the region, and insert the key pressed." (interactive) (let ((c (aref (recent-keys) (1- (length (recent-keys)))))) (if (eq (char-after) c) (forward-char 1) (insert c) (if (and (symbolp 'rl-hilight-mark) rl-hilight-mark (not (= (point) rl-hilight-mark))) (progn (kill-region (point) rl-hilight-mark) (setq rl-hilight-mark nil)))))) (defun cddb-readline-minibuffer-hook () "Hook for autocomplete-style minibuffer hack. Stores the transient mark (rl-hilight-mark), enables `transient-mark-mode', and sets mark to the end of the buffer." (make-variable-buffer-local 'rl-hilight-mark) (setq rl-hilight-mark (+ 1 (point-max))) (make-variable-buffer-local 'transient-mark-mode) (transient-mark-mode 1) (set-mark (point-max))) (defun cddb-read-from-minibuffer( prompt &optional initial-contents keymap read hist default-value inherit-input-method) "Autocomplete version of `read-from-minibuffer' Provides similar functionality to Windows-based autocomplete typeahead, the default text is highlighted; typing the same text progressively unhighlights the text until a non-matching character is typed, at which point the rest of the line is erased." (let ((mbsh-orig minibuffer-setup-hook) new-keymap) ;; If INITIAL-CONTENTS is unset, no point in any of this trousers (if (or (null initial-contents) (if (stringp initial-contents) (string= "" initial-contents) (string= "" (car initial-contents)))) (read-from-minibuffer prompt initial-contents keymap read hist default-value inherit-input-method) ;; If the starting point on INITIAL-CONTENTS is unset, set it to 0 (or (listp initial-contents) (setq initial-contents (cons initial-contents 0))) ;; patch the keymap (if (null keymap) (setq keymap minibuffer-local-map)) ;; I'm using copy-keymap because I've already managed to chew a ;; hole in one keymap that wasn't mine... (setq new-keymap (copy-keymap keymap)) (substitute-key-definition 'self-insert-command 'cddb-keymap-maybe-kill new-keymap global-map) ;; Of course, if you've already messed up self-insert-command, ;; we're all doomed, you know. ;; unwind-protect to make sure we clean up the hook afterwards (unwind-protect (progn (add-hook 'minibuffer-setup-hook 'cddb-readline-minibuffer-hook) (read-from-minibuffer prompt initial-contents new-keymap read hist default-value inherit-input-method)) (setq minibuffer-setup-hook mbsh-orig))))) ;;; ;;; End of autocomplete stuff ;;; ;; Actual mode function (defun cddb-mode() "Mode for editing CDDB files" (interactive) (setq major-mode 'cddb-mode mode-name "CDDB") ;; make the Revision automatically bump up if the file's modified. (add-hook 'local-write-file-hooks 'cddb-bump-revision) ;; CDDB files are ISO-8859-1 (setq buffer-file-coding-system 'iso-8859-1) ;; Is this a multi-artist CD? (make-variable-buffer-local 'cddb-grip-multi-artist) (save-excursion (goto-char (point-min)) (setq cddb-grip-multi-artist (re-search-forward "^TARTIST[0-9]+=" (point-max) t))) ;; Determine if we should kick straight into edit mode ;; Three attempts to write the regexp, one of which was a typo. ;; On the downside, this isn't the most user-friendly way of ;; operating. I shouldn't use the minibuffer for all the data entry. ;; (save-excursion ;; (goto-char (point-min)) ;; (if (re-search-forward ;; "^DTITLE=\\($\\|\\s-*/\\s-*\\(Unknown\\|\\s-*$\\)\\)" ;; (point-max) t) ;; (cddb-edit)))) ) ;; Assorted editing functions (defun cddb-edit-artist() "Edit the artist/album field" (interactive) (save-excursion (goto-char (point-min)) (let ((title "") (ntitle "")) (if (re-search-forward "^DTITLE=\\(.*\\)$" (point-max) t) (setq title (buffer-substring (match-beginning 1) (match-end 1)))) (setq ntitle (cddb-read-from-minibuffer "Artist / Album: " (cons title 0) nil nil nil title nil)) (if (string= title ntitle) () (delete-region (match-beginning 1) (match-end 1)) (goto-char (match-beginning 1)) (insert ntitle))))) (defun cddb-edit-genre() "Edit the genre field" (interactive) (save-excursion (goto-char (point-min)) (let ((genre "") ngenre) (if (re-search-forward "^DGENRE=\\(.*\\)$" (point-max) t) (setq genre (buffer-substring (match-beginning 1) (match-end 1)))) (setq ngenre (cddb-read-from-minibuffer "Genre: " (cons genre 0) nil nil nil genre nil)) (if (string= genre ngenre) () (delete-region (match-beginning 1) (match-end 1)) (goto-char (match-beginning 1)) (insert ngenre))))) (defun cddb-edit-year() "Edit the year field This is strictly a Gronk/Grip thing. It's not a real CDDB field. YHBW." (interactive) (save-excursion (goto-char (point-min)) (let ((year "") nyear) (if (re-search-forward "^DYEAR=\\(.*\\)$" (point-max) t) (setq year (buffer-substring (match-beginning 1) (match-end 1)))) (setq nyear (cddb-read-from-minibuffer "Year: " (cons year 0) nil nil nil year nil)) (if (string= year nyear) () (if (markerp (match-beginning 1)) (progn (delete-region (match-beginning 1) (match-end 1)) (goto-char (match-beginning 1))) (setq nyear (concat nyear "\n")) (goto-char (point-max)) (or (eolp) (insert "\n")) (insert "DYEAR=")) (insert nyear))))) (defun cddb-edit-tracklist() "Edit the tracklist" (interactive) (save-excursion (goto-char (point-min)) (while (re-search-forward "^TTITLE\\([0-9]+\\)=\\(.*\\)$" (point-max) t) (let ((p (buffer-substring (match-beginning 2) (match-end 2))) (tnum (buffer-substring (match-beginning 1) (match-end 1))) n) (setq n (cddb-read-from-minibuffer (concat "Rename " p " to: ") (cons p 0) nil nil nil p nil)) (if (string= p n) () (delete-region (match-beginning 2) (match-end 2)) (goto-char (match-beginning 2)) (insert n)) (forward-char 1) (if cddb-grip-multi-artist (let (a na) (if (looking-at "^TARTIST[0-9]+=\\(.*\\)$") (setq a (buffer-substring (match-beginning 1) (match-end 1))) (setq a "")) (setq na (cddb-read-from-minibuffer (concat "Artist for " n ": ") a nil nil nil a nil)) (if (string= a na) () (if (looking-at "^TARTIST[0-9]+=\\(.*\\)") (progn (delete-region (match-beginning 0) (match-end 0)) (goto-char (match-beginning 0)))) (insert (concat "TARTIST" tnum "=" na)) (or (looking-at "\n") (insert "\n"))))))))) (defun cddb-edit-extras() "Edit the EXTTNN fields" (interactive) (save-excursion (goto-char (point-min)) (while (re-search-forward "^EXTT\\([0-9]+\\)=\\(.*\\)$" (point-max) t) (let ((p (buffer-substring (match-beginning 2) (match-end 2))) (tnum (buffer-substring (match-beginning 1) (match-end 1))) n) (setq n (cddb-read-from-minibuffer (format "Extended information for track %d: " (+ 1 (string-to-int tnum))) (cons p 0) nil nil nil p nil)) (if (string= p n) () (delete-region (match-beginning 2) (match-end 2)) (goto-char (match-beginning 2)) (insert n)))))) (defun cddb-fold-grip-format() "Fold Grip's multi-artist format into something that everyone else can deal with" (interactive) (save-excursion (if (not cddb-grip-multi-artist) (error "This isn't a multi-artist file")) (goto-char (point-min)) (while (re-search-forward "^TARTIST\\([0-9]+\\)=\\(.*\\)$" (point-max) t) (let ((p (buffer-substring (match-beginning 2) (match-end 2))) (tnum (buffer-substring (match-beginning 1) (match-end 1)))) (save-excursion (goto-char (point-min)) (if (re-search-forward (concat "^TTITLE" tnum "=")) (insert (concat p " / ")))) ;; nuke it (beginning-of-line) (let ((start (point))) (forward-line 1) (kill-region start (point))))))) (defun cddb-edit() "Edit the current buffer as a CDDB file" (interactive) (message "Editing entire buffer") (cddb-edit-artist) (cddb-edit-genre) (cddb-edit-year) (cddb-edit-tracklist)) ;; Write hook, to increment the revision field (defun cddb-bump-revision() "Bump the revision number of this file" (interactive) (save-excursion (goto-char (point-min)) (let ((revision "") mb1 me1) (if (and (re-search-forward "# Revision: \\(.*\\)$" (point-max) t) (setq mb1 (match-beginning 1) me1 (match-end 1) revision (buffer-substring mb1 me1)) (string-match "^[0-9]+$" revision)) (progn (setq revision (+ 1 (string-to-int revision))) (delete-region mb1 me1) (goto-char mb1) (insert (int-to-string revision))))))) (defun cddb-edit-newest() "Edit newest addition to ~/.cddb directory" (interactive) (let ((file (file-newest-directory (expand-file-name "~/.cddb") t "^[a-f0-9]*$"))) (if (null file) (error "No files found in ~/.cddb") (find-file file) (cddb-edit)))) (defun file-newest-directory( directory &optional full match) "Return the name of the newest file in DIRECTORY. If FULL is specified, return the full name. If MATCH is specified, only return files which match it." (let ((files (directory-files-and-attributes (expand-file-name directory) full match)) (newest nil)) (while files (if newest (if (> (float-time (nth 5 (car files))) (float-time (nth 5 newest))) (setq newest (car files))) (setq newest (car files))) (setq files (cdr files))) (nth 0 newest))) (defun cddb-validate-buffer() "Check that the current buffer is valid per the CDDB spec." (interactive) (goto-char (point-min)) (while (not (eobp)) (beginning-of-line) (let ((start (point))) (if (eolp) (error "blank lines not permitted")) (if (looking-at "[^#]") () ;; full iso8859-1 range allowed - check find-charset-region (skip-chars-forward "\t[\x20-\x7E]") (if (not (eolp)) (error "invalid character in comment"))) (if (> (current-column) 255) (error "line too long")) ;; put format checks here ;; next line (forward-line) ))) (defun cddb-find-file( track ) (interactive "sTrack to find " ) (grep (format "\"%s\"" track) (expand-file-name "~/.cddb/*"))) ;;; Local Variables: *** ;;; time-stamp-start:"Last Modified:[ ]+" *** ;;; time-stamp-end: "$" *** ;;; time-stamp-format:"Waider / %02d/%02m/%:y" *** ;;; End: ***