;;; Code for editing ID3 tags (version 0.01a, 2002-10-15). ;;; Copyright (C) 2002 Stefan D. Bruda ;;; ;;; This is part of the mode for managing a multimedia library. ;;; ;;; This program 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 1, or (at your option) ;;; any later version. ;;; ;;; This program 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, write to the Free Software ;;; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. ;;; ;;; ;;; Commentary: ;; ;; Just place the file where Emacs can see it, and that's it. The ;; loading is taken care of in the `music.el' code. ;; ;; This is a subsystem that allows for the manipulation if ID3 tags ;; in MP3 files. It offers functions for importing and exporting ;; information from/to these tags. Do not use it on files that do ;; not support ID3 tags. ;; ;; CAUTION: This is an alpha version, use it at your own risk. In ;; particular, ;; ;; o The subsystem opens the MP3 files in Emacs buffers. If your ;; Emacs does not save files as they are (i.e., in binary mode), ;; DO NOT USE THESE FUNCTIONS, as they will damage the file. ;; ;; o As well, it is a very bad idea to use this code on files that ;; do not support ID3 tags, since the writing function writes ;; such a tag anyway. If you mistakenly appended a tag to a file ;; that does not like this, use `music-id3-delete-tag' (also ;; provided in the menu) to get rid of the thing. ;; ;; This subsystem uses the customizable variable ;; `music-album-name-separator' which is defined in the main file. ;; ;; Functions are poorly commented, I will change this as soon as I ;; find time. (defvar music-id3-version "0.01a") (require 'music) ;; SECTION 0: Key bindings and menu: (defun music-id3-initialize () "Initializes the ID3 manipulation system: inserts the menu and adds some key bindings." (interactive) (define-key music-mode-map [(control c) i] 'music-id3-import-this-track) (define-key music-mode-map [(control c) t] 'music-id3-export-this-track-using-current-album) (define-key music-mode-map [(control c) e] 'music-id3-export-whole-album) (define-key music-mode-map [(control c) T] 'music-id3-export-this-track-ask-album) (music-id3-install-menu)) (easy-menu-define music-id3-menu (list music-mode-map) "The menu for manipulation of ID3 tags. Also see `music-mode-map'." '("ID3" ["Import track name from ID3" (music-id3-import-this-track) :active (music-get-track) :keys "C-c i"] ["Import album name from ID3" (music-id3-import-album) :active (or (music-get-album) (music-get-track)) :keys "C-c a"] "---" ["Export album to ID3" (music-id3-export-whole-album) :active (or (music-get-album) (music-get-track)) :keys "C-c e"] ["Export track to ID3 (take name and artist from album title)" (music-id3-export-this-track-using-current-album) :active (music-get-track) :keys "C-c t"] ["Export track to ID3 (prompt for album, artist)" (music-id3-export-this-track-ask-album) :active (music-get-track) :keys "C-c t"] "---" ["Erase the ID3 tag of this track" (music-id3-delete-tag) :active (music-get-track)] )) (defun music-id3-install-menu () "Installs `music-id3-menu', the menu for ID3 tags manipulation." (easy-menu-remove music-id3-init-menu) (easy-menu-add music-id3-menu)) ;;; SECTION 1: Import functions. (defun music-id3-import-album () (interactive) (let* ((filename (music-get-track)) (dir (music-prev-album)) (id3 (music-id3-read-tag filename dir)) (artist (music-id3-get-artist id3)) (album (music-id3-get-album id3)) (title (concat artist music-album-name-separator album)) here ) (unless (car id3) (error "Could not read ID3 info.")) (save-excursion (music-show-album-info) (beginning-of-buffer) (setq here (point)) (end-of-line) (delete-region here (point)) (insert title) (music-info-quit) ))) (defun music-id3-import-this-track () (interactive) (let ((filename (music-get-track)) (dir (music-prev-album)) song-name ) (unless (and filename dir) (error "Not on a track.")) (setq song-name (car (music-id3-read-tag filename dir))) (unless song-name (error "No ID3 tag found.")) (unless (music-find-track-file dir) (let ((files (directory-files dir nil nil nil t)) (ls-result (music-find-track-file dir))) ;; Create the tracks file... (dolist (ff (append music-title-files music-track-files)) ;; ignore special files. (setq files (delete ff files))) (save-excursion (when (get-buffer " ***Music Temp***") (kill-buffer " ***Music Temp***")) (generate-new-buffer " ***Music Temp***") (set-buffer " ***Music Temp***") (insert (mapconcat '(lambda (str) str) files "\n") "\n") (append-to-file (point-min) (point-max) (expand-file-name ls-result dir)) (kill-buffer " ***Music Temp***")))) ;; we have now a tracks file to insert things into... (save-excursion (music-show-tracks-info) ;; now in edit buffer... (beginning-of-buffer) (if (or (progn ;file on first line? (forward-char (length filename)) (equal (buffer-substring (point-min) (point)) filename) ) (search-forward (concat "\n" filename) nil t)) (progn ; entry found... ;(insert ">>>" (buffer-substring (point) (+ (point 4))) "<<<" ) (when (equal " ++ " (buffer-substring (point) (+ (point) 4))) (let ((here (point))) (end-of-line) (delete-region here (point)))) (insert " ++ " song-name)) (progn ; insert new entry (end-of-buffer) (insert "\n" filename " ++ " song-name))) ;(music-info-save-quit) ) )) ;;; SECTION 2: Export functions. (defun music-id3-export-whole-album () (interactive) (save-excursion (cond ((and (music-get-album) (equal (music-collapsed-p) 'collapsed)) (music-expand)) ((music-get-track) (music-goto-prev-album)) ((music-get-album)) (t (error "Not on a track or album."))) (forward-line 1) (while (not (or (music-get-album) (music-get-folder))) (music-id3-export-this-track-using-current-album) (forward-line 1)))) (defun music-id3-export-this-track-using-current-album () (interactive) (let* ((track (music-get-track)) (dir (music-prev-album)) (song-name (music-get-name)) (old-data (music-id3-read-tag track dir)) ;(song-name artist album year comment genre) (a-a (save-excursion (music-goto-prev-album) (music-id3-get-album-name))) ) (unless (and track dir) (error "Not on a track.")) (apply 'music-id3-write-tag track dir (mapcar* '(lambda (x y) (if (and x (not (equal x ""))) x y)) (append (list song-name) a-a (list nil nil nil)) old-data) ) )) (defvar music-id3-last-artist "" "Keeps the last user input for artist name. Used by `music-id3-export-this-track-ask-album'.") (defvar music-id3-last-album-name "" "Keeps the last user input for album name. Used by `music-id3-export-this-track-ask-album'.") (defvar music-id3-last-year "" "Keeps the last user input for year. Used by `music-id3-export-this-track-ask-album'.") (defvar music-id3-last-comment "" "Keeps the last user input for comment. Used by `music-id3-export-this-track-ask-album'.") (defvar music-id3-last-genre 13 "Keeps the last user input for gendre. Used by `music-id3-export-this-track-ask-album'.") (defun music-id3-export-this-track-ask-album () (interactive) (let* ((prev-album (save-excursion (music-goto-prev-album) (music-id3-get-album-name))) (music-id3-last-artist (or (and prev-album (car prev-album)) music-id3-last-artist)) (music-id3-last-album-name (or (and prev-album (cadr prev-album)) music-id3-last-album-name)) (track (music-get-track)) (dir (music-prev-album)) (song-name (music-get-name)) (old-data (music-id3-read-tag track dir)) ;(song-name artist album year comment genre) (artist (setq music-id3-last-artist (read-string "Artist: " music-id3-last-artist nil music-id3-last-artist))) (album (setq music-id3-last-album-name (read-string "Album name: " music-id3-last-album-name nil music-id3-last-album-name))) ;(year (setq music-id3-last-year ; (read-string "Year: " music-id3-last-year nil music-id3-last-year))) (year "") ;(comment (setq music-id3-last-comment ; (read-string "Comment: " music-id3-last-comment nil music-id3-last-comment))) (comment "") ;(genre (setq music-id3-last-genre ; (read-number "Genre (between 1 and 115) [13]: " ; t (int-to-string music-id3-last-genre)))) (genre 13) (a-a (list song-name artist album year comment genre)) ) (unless (and track dir) (error "Not on a track.")) (apply 'music-id3-write-tag track dir (mapcar* '(lambda (x y) (if (and x (not (equal x ""))) x y)) a-a old-data) ) )) ;;; SECTION 3: Code for actually reading and writing ID3 tags from/to ;;; MP3 files. (defun music-id3-read-tag (file &optional dir) "Retrieves and returns the content of the ID3 tag at the end of FILE from directory DIR (default directory if DIR is nil). The returned value is the list (TRACK-NAME ARTIST ALBUM-NAME YEAR COMMENT GENRE) The following format is assumed for the ID3 tag: Field Length Offset Returned type -----------+------+------+------------- TAG 3 0 n/a Track name 30 3 string Artist 30 33 string Album name 30 63 string Year 4 93 string Comment 30 97 string Genre 1 127 integer -----------+------+------+-------------" (let ((filename (expand-file-name file dir)) song-name artist album year comment genre) (when (file-exists-p filename) (when (get-buffer "**ID3 Edit Buffer**") (kill-buffer "**ID3 Edit Buffer**")) (generate-new-buffer "**ID3 Edit Buffer**") (save-excursion (set-buffer "**ID3 Edit Buffer**") (insert-file-contents filename) (end-of-buffer) (goto-char (- (point) 128)) (when (equal (buffer-substring (point) (+ (point) 3)) "TAG") (setq song-name (music-id3-trim (buffer-substring (+ (point) 3) (+ (point) 33))) artist (music-id3-trim (buffer-substring (+ (point) 33) (+ (point) 63))) album (music-id3-trim (buffer-substring (+ (point) 63) (+ (point) 93))) year (music-id3-trim (buffer-substring (+ (point) 93) (+ (point) 97))) comment (music-id3-trim (buffer-substring (+ (point) 97) (+ (point) 127))) genre (string-to-char (music-id3-trim (buffer-substring (+ (point) 127) (+ (point) 128)))) genre (if genre (char-int genre) 255) )) (kill-buffer "**ID3 Edit Buffer**"))) (list song-name artist album year comment genre))) (defun music-id3-write-tag (file &optional dir song-name artist album year comment genre) "Modifies/creates a new ID3 tag for file FILE in directory DIR (default directory if DIR is NIL. See `music-id3-read-tag' for the format of the written ID3 tag." (let* ((filename (expand-file-name file dir)) (genre (if genre genre 13)) (id3-tag (concat "TAG" (music-id3-pad-to song-name 30) (music-id3-pad-to artist 30) (music-id3-pad-to album 30) (music-id3-pad-to year 4) (music-id3-pad-to comment 30) (char-to-string (int-char genre)) ) )) (when (file-exists-p filename) (find-file filename) (end-of-buffer) (goto-char (- (point) 128)) (when (equal (buffer-substring (point) (+ (point) 3)) "TAG") (delete-region (point) (point-max))) (insert id3-tag) (save-buffer) (kill-buffer nil)))) (defun music-id3-delete-tag () (interactive) (let ((file (music-get-track)) (dir (music-prev-album))) (unless (and file dir) (error "Not on a track.")) (let ((filename (expand-file-name file dir))) (when (file-exists-p filename) (find-file filename) (end-of-buffer) (goto-char (- (point) 128)) (if (equal (buffer-substring (point) (+ (point) 3)) "TAG") (when (y-or-n-p (concat "Really erase the ID3 tag of " file "? ")) (delete-region (point) (point-max))) (message (concat "No ID3 tag found in " file "."))) (save-buffer) (kill-buffer nil))))) ;;; SECTION 4: Miscelaneous functions. (defun music-id3-get-album-name (&optional separator) "Returns the name and artist of the current album, NIL on albums and folders." (save-excursion (beginning-of-line) (let ((here (point)) there (sep (if separator separator music-album-name-separator))) (end-of-line) (setq there (point)) (when (and (search-backward "**" here t) (search-backward "**" here t)) (setq there (point)) (search-backward-regexp "-[ 0-9%][ 0-9%]-" here t) (search-backward "----" here t) (setq here (+ (point) 4)) (if (search-forward sep there t) (list (music-id3-trim (buffer-substring here (- (point) (length sep)))) ; artist (music-id3-trim (buffer-substring (point) there)) ; album ) (list (music-id3-trim (buffer-substring here there)) nil)) )))) (defun music-goto-prev-album () "Returns the absolute path of the nearest album above point, places point on the album line." (let (aalbum) (forward-line -1) (setq aalbum (music-get-album)) (while (not aalbum) (forward-line -1) (setq aalbum (music-get-album))) aalbum)) (defun music-id3-get-track (info) (car info)) (defun music-id3-get-artist (info) (car (cdr info))) (defun music-id3-get-album (info) (car (cddr info))) (defun music-id3-get-year (info) (car (cdddr info))) (defun music-id3-trim (string) "Eliminates all the blank characters from the beginning and the end of STRING." (replace-in-string (replace-in-string string "^[ ]+" "") "[ ]+$" "")) (defun music-id3-pad-to (string length) "Pads STRING with blanks to the right until it reaches length LENGTH. If the length of the string is larger than LENGTH, trims it to LENGTH characters." (if (> (length string) length) (substring string 0 length) (let ((padding "")) (dotimes (i (- length (length string))) (setq padding (concat " " padding))) (concat string padding)))) (defun music-id3-erase-timestring (track) "Erases all the constructions [xy:yy] from string TRACK and returns the result, where x is either a blank or a digit, and y is a digit." (music-id3-trim (replace-in-string track "\[[ 0-9]?[0-9]?:[0-9]?[0-9]?\]" ""))) ;;; SECTION 4: Development, not currently used: (defun* music-id3-expand-key (str &key (track "") (artist "") (album "") (year "") (comment "") (genre "")) (music-id3-expand str track artist album year comment genre)) (defun* music-id3-expand (str &optional (track "") (artist "") (album "") (year "") (comment "") (genre "")) "Expands % constructions: Track name %s Artist %a Album %d Year %y Comment %c Genre %g % %% " (let ((ret (replace-in-string str "%s" track))) (setq ret (replace-in-string ret "%a" artist) ret (replace-in-string ret "%d" album) ret (replace-in-string ret "%y" year) ret (replace-in-string ret "%c" comment) ret (replace-in-string ret "%g" genre) ret (replace-in-string ret "%%" "%")))) ;; Make myself known. (provide 'music-id3)