IdEst – ID3 Editing and Scripting Tool (split by chapter):   Section:   Chapter:FastBack: Structure   Up: Top   FastForward: Backups   Contents: Table of ContentsIndex: Concept Index

10 Scripting

Idest offers a scripting facility, which makes it possible to extend its functionality beyond the basic operations, described in previous chapters. Scripts must be written in Scheme, using ‘Guile’, the GNU’s Ubiquitous Intelligent Language for Extensions. For information about the language, refer to Revised(5) Report on the Algorithmic Language Scheme. For a detailed description of Guile and its features, see Overview in The Guile Reference Manual.

The scripting mode is enabled when the option --script (-S) is given in the command line. This option stops further option processing, so any other idest command line options must be given before it. The argument to this option specifies the name of the script file:

$ idest --script list.scm *.mp3

You can omit the ‘.scm’ suffix, as idest will try it automatically (see below).

When this option is given, the following operations are performed:

  1. The program looks for files .idest.scm, $HOME/.idest.scm and guile-site-dir/idest/idest.scm in that order. Here, ‘$HOME’ stands for the user home directory and guile-site-dir stands for the Guile site-wide directory, as described in guile-site-dir. If any of these files is found, it is loaded as a Scheme source code and further search is discontinued. This allows you to configure Guile settings on per-directory, per-user and site-wide basis.

    This step is omitted if the program is given the --no-init-files (-N) option.

    When a startup file is loaded, the list of files which were to be tried after it is passed to it as arguments. This allows for chain-loading all files in the list using the following code:

    (let load-loop ((name-list (cdr (command-line))))
      (if (not (null? name-list))
          (let ((name (car name-list)))
    	(load-loop (cdr name-list))
    	(if (file-exists? name)
    	    (primitive-load name)))))
    
  2. Unless the supplied script name contains directory separators (‘/’), it is searched in the Guile’s %load-path. The default load path is formed as follows:
    version-site-dir
    .
    package-site-dir
    guile-site-dir
    %load-path
    

    where the components are as follows:

    %load-path

    The standard Guile load path (see Build Config in The Guile Reference Manual).

    guile-site-dir

    This directory is selected at compile time using the rules below. Its value is returned by the (%idest-guile-site-dir) primitive:

    1. Determine actual value of the default Guile site directory, by inspecting the value returned by the ‘%site-dir’ primitive.
    2. If that value lies under the current installation prefix, use it.
    3. Otherwise, if the --with-guile-site-dir option is supplied:
      1. If it is used without arguments, use the ‘%site-dir’ value.
      2. Otherwise, the value of this option is taken as new site directory.
    4. Otherwise, a warning is issued and $(datadir)/guile/site is used as the site directory.

    The reason for using this directory is described in http://www.gnu.org.ua/software/gint/#guile-site-dir.

    If guile-site-dir coincides with the standard %site-dir, this part is omitted, because the latter is always present in the %load-path.

    package-site-dir

    This is the directory for installing version-independent idest files. It is formed as follows:

    guile-site-dir/idest
    

    This value is returned by the (%idest-package-site-dir) primitive.

    version-site-dir

    This is the directory for installing version-dependent idest files. It is formed as follows:

    package-site-dir/2.1
    

    This value is returned by the (%idest-version-site-dir) primitive.

    The load path can be modified using the --load-path (-P) and --prepend-load-path (-p) command line options. Both options take as argument a list of directory names, separated by colons. The --load-path option adds these directories to the tail of the load path list. The --prepend-load-path option adds them to the head of the load path list.

    The script is loaded via primitive-load-path (see primitive-load-path in The Guile Reference Manual), so idest will consult the %load-extensions list and try suffixes from that list as described in %load-extensions in The Guile Reference Manual).

  3. The script is read and evaluated.

    The script can access command line arguments via the usual command-line function (see command-line in The Guile Reference Manual). It can also modify the argument list (e.g. by removing its command line options). It must not, however, modify ‘argv[0]’. Any changes it does to the argument list become visible to idest. The only requirement is that the modified argument list consist of the script name (as argv[0]) and input file names.

  4. The script’s main function is applied to each input file in turn.

The main function must be declared as:

Function: idest-main file frames

It takes two arguments. The file argument supplies the name of the file being processed. The frames argument is a list of ID3 frames read from that file. Each element of frames is a pair, with the frame name in its car and an association list of frame properties in its cdr.

The properties are identified by property names, which are Scheme symbols. The following property names are defined:

text

Value of this frame, as a string.

descr

Frame description. It is a string, verbosely describing the frame. For example, the description of ‘TRCK’ frames is ‘Track number/position in set’.

These are the same descriptions that are output with the --describe option (see describe).

rawdata

Unsupported or partially-supported frames contain only this property. Its value is a list of frame fields. Each field is represented by a triplet ‘(ord type value)’, where ord is the ordinal number of that field in frame, type is its type (integer) and value is its value. If type is one of numeric types, value is the numeric value converted to string (as per number->string). If type is a string type, value contains the string in the appropriate encoding. Otherwise, value holds the field value as a binary string. Each byte in such a string is represented by two hexadecimal digits. For example, ‘AB\n’ is represented as ‘41420A’.

More properties are defined at a per-frame basis to represent frame qualifiers. They are named after corresponding qualifiers as listed in --list-frames output (see describe). For example, for ‘comment’ (‘COMM’) frames:

lang

A three-letter code of the language in which the text is written.

condesc

Content descriptor.

The mode in which input files are open is controlled by the idest-readonly variable:

Variable: idest-readonly

This is a boolean variable that indicates whether idest-main can modify tag frames. If its value is #t, the function return value will be ignored, and input files will be opened in read-only mode. This is the default.

If idest-readonly is ‘#f’ the idest-main function should return the new list of frames. If it returns an empty list, all existing frames will be deleted. If the function chooses not to modify any frames, it must return #f.

The two following sections show how to write script files. The sample scripts they discuss can be found in subdirectory examples of the idest distribution.

10.1 Using Scripts to List ID3 Frames

This section illustrates how to use the scripting facility for listing the contents of ID3 tags.

The simplest way to list all frames using a Guile script is:

;; list1.scm -- lists all frames.
(define (idest-main name frames)
  (display name)
  (newline)
  (for-each
   (lambda (frame)
     (display frame)
     (newline))
   frames))

Here is a sample output:

$ idest --script list1.scm track01.scm
track01.mp3
(TIT2 (descr . Title/songname/content description)
      (text . Cor i arbre))
(TRCK (descr . Track number/position in set)
      (text . 1))
(COMM (descr . Comments) (condesc . Bit_Rate)
      (lang . eng) (text . 320))
(TENC (descr . Encoded by) (text . Myencoder 1.0))
(COMM (descr . Comments) (condesc . Sample_Rate)
      (lang . eng) (text . 44100))

As mentioned above, a script can access the command-line arguments. To illustrate this, let’s modify the list1.scm to display only a subset of frames, given as a comma-separated list in the first argument. To do so, we will need a list of requested frames:

(define frame-list '())

The main function consults this list to see whether to display a frame:

(define (idest-main name frames)
  (display name)
  (newline)
  (for-each
   (lambda (frame)
     (if (member (car frame) frame-list)
	 (begin
	   (display frame)
	   (newline))))
   frames))

Finally, the following code initializes frame-list from the first argument and removes that argument from the list seen by idest. Note that the 0th argument is the name of the script itself, and it should not be modified.

(let ((cmd (command-line)))
  (cond
   ((< (length cmd) 3)
    (error "usage: idest -S list2 FRAME-LIST FILE...")
    (exit 1))
   (else
    (set! frame-list (string-split (list-ref cmd 1) #\,))
    (set-program-arguments (cons (car cmd)
                           (list-tail cmd 2))))))

The full script text is then:

;; list2.scm -- lists only requested frames.
(define frame-list '())

(define (idest-main name frames)
  (display name)
  (newline)
  (for-each
   (lambda (frame)
     (if (member (car frame) frame-list)
	 (begin
	   (display frame)
	   (newline))))
   frames))

(let ((cmd (command-line)))
  (cond
   ((< (length cmd) 3)
    (error "usage: idest -S list2 FRAME-LIST FILE...")
    (exit 1))
   (else
    (set! frame-list (string-split (list-ref cmd 1) #\,))
    (set-program-arguments (cons (car cmd)
                                 (list-tail cmd 2))))))

Sample usage:

$ idest --script list2 TIT2,TENC track01.scm
(TIT2 (descr . Title/songname/content description)
      (text . Cor i arbre))
(TENC (descr . Encoded by) (text . Myencoder 1.0))

A more elaborate example will print, for each input file, its name, followed by the title, artist name and year, as shown in this sample output:

$ idest -S shortlist *.mp3
dnr.mp3: Diamonds & Rust by Joan Baez, 1975
ams.mp3: Amsterdam, by Jacques Brel, 1968

To implement this, we would need a function that returns the value of a given frame from the frame list. Remember, that the latter is a list of pairs, so the task is achieved easily by using the assoc-ref function:

(define (get-frame code frames)
  (or (assoc-ref
       (or (assoc-ref frames code) '())
       'text)
      "unknown"))

The inner assoc-ref selects a requested frame. An empty list is returned if such a frame is not found. The outer assoc-ref selects the ‘text’ property.

Now, we define the main function:

(define (idest-main name frames)
  (format #t "~A: ~A by ~A, ~A~%"
	  name
	  (get-frame "TIT2" frames)   ; Title
	  (get-frame "TPE1" frames)   ; Artist
	  (get-frame "TDRC" frames))) ; Year

10.2 Using Scripts to Modify ID3 Frames

This section illustrates how to write scripts that modify ID3 tags. We will write a script which creates a new value for the ‘title’ (TIT2) frame from the name of the input file. The title is created using the following algorithm:

  1. Strip off leading directories and the ‘.mp3’ suffix.
  2. Replace underscores with spaces.

Here is the implementation:

;; settitle.scm - set title (TIT2) frame based on
;; the file name.

(use-modules (ice-9 regex)
	     (srfi srfi-13))

(define (idest-main file frames)
  (cond
   ((string-match "(.*)\\.mp3" file) =>
    (lambda (match)
      (cons
       (cons "TIT2"
	     (list
	      (cons
	       'text
	       (string-map
		(lambda (c)
		  (if (char=? c #\_) #\space c))
		(match:substring match 1)))))
       ;;
       (filter
	(lambda (elt)
	  (not (string=? (car elt) "TIT2")))
	frames))))
   (else
    #f)))

(set! idest-readonly #f)

An example of using this script on all files in the current directory:

$ idest --script settitle *.mp3

10.3 Format

Formats are advanced scripting feature which allows for extending idest output by writing an appropriate script in Scheme. A format is invoked using the --format (-H) command line option. The format name is given as argument to that option. Similarly to the --source option, the --format option stops further argument processing and passes the rest of arguments to the format module, which is supposed to remove its option arguments and leave only input file names. For example:

$ idest --format=framelist -Q -l *.mp3

This example invokes idest with the ‘framelist’ format (see framelist). The -Q and -l flags are format options.

10.3.1 How to Write Format Modules

The source for format module name must be saved in the file named name.scm located in the subdirectory idest/format somewhere in the Guile load path. It must begin with the following clause:

(define-module (idest format name))

The module must define and export the ‘idest-main’ function, whose calling convention and return type is the same as that in the usual idest scripts (see idest-main). For example, the following is a simplified version of the ‘framelist’ module (see framelist):

(define-module (idest format framelist))

(define frame-list '())

(define-public (idest-main name frames)
  (for-each
    (lambda (elt)
      (cond
       ((member (car elt) frame-list)
        (display (car elt))
        (newline))))
    frames))

If the module needs to process command line arguments, it may not do so in the main code, as the usual idest modules do. Instead, it should export a special function, ‘idest-init’, defined as:

(define-public (idest-init)
  ...)

This function analyzes the command line, removes the consumed modules options and returns. For example:

(define-public (idest-init)
  (let ((cmd (command-line)))
    (cond
     ((< (length cmd) 3)
      (error "usage: idest --format=framelist
             FRAME-LIST FILE...")
      (exit 1))
     (else
      (set! frame-list (string-split (list-ref cmd 1) #\,))
      (set-program-arguments
        (cons (car cmd) (list-tail cmd 2)))))))

The module should also export the symbol ‘description’, which should contain a string with a concise description of the module. This description will be shown in the --format=help output (see help format). For example:

(define-public description
  "display a list of frames defined in each file")

10.3.2 Existing Formats

Idest is shipped with a set of predefined formats. These formats are found in the scheme/idest/format subdirectory of the source tree. They are installed into the version-site-dir’/format directory (see version-site-dir).

10.3.2.1 help: List and Describe Available Formats

The ‘help’ format searches the load path for available format modules and lists them. For each module its name and short description are shown on a separate line. The output is sorted alphabetically by the format name:

$ idest --format=help
framelist: display a list of frames defined in each file
lyrics: display lyrics (the USLT content), if present
pic: show attached picture (APIC frame) or save it on disk
shortlist: display title, artist name and year

If ‘help’ is used with the --which (-w) option, the format includes the directory where the module is found:

$ idest --format=help --which
framelist (/usr/share/idest/format): display a list of frames
defined in each file
...

10.3.2.2 framelist: Display List of Frames Present in Each File

The ‘framelist’ format displays a list of ID3 frames present in each input file, e.g.:

$ idest --format=framelist file.mp3
TIT2
TRCK
COMM
TENC
COMM

The following command line options are understood:

-F
--full

Display all qualifiers. For example:

$ idest --format=framelist --full file.mp3
TIT2 descr="Title/songname/content description"
TRCK descr="Track number/position in set"
COMM descr="Comments" lang="eng" condesc=""
TENC descr="Encoded by"
COMM descr="Comments" lang="cat" condesc=""
-f flist
--frames flist

Display only frames from flist, which is a list of frame names, separated by commas.

-Q
--qualified

Display frames in qualified form:

$ idest --format=framelist --qualified file.mp3
TIT2
TRCK
COMM:eng:
TENC
COMM:cat:
-l
--single-line

Fit output on single-line, e.g.:

$ idest --format=framelist --single-line file.mp3
TIT2,TRCK,COMM,TENC,COMM
-h
--help

Show a short help summary

10.3.2.3 lyrics: Display Lyrics

The ‘lyrics’ format displays the lyrics (as found in the ‘USLT’ frame). The text is preceded by the song title from the ‘TIT2’ frame, e.g.:

$ idest --format lyrics file.mp3
How doth the little

How doth the little crocodile
Improve his shining tail,
And pour the waters of the Nile
On every golden scale!

How cheerfully he seems to grin,
How neatly spreads his claws,
And welcomse little fishes in
With gently smiling jaws!

If the environment variable PAGER is set, its value is used to paginate the output.

This module supports the following command line options:

-l name
--lang name

Select ‘USLT’ frames with name as the value of their ‘lang’ qualifier.

-c text
--content text

Select ‘USLT’ frames with text as the value of their ‘condesc’ qualifier.

-h
--help

Show a short help summary

10.3.2.4 pic: Display Attached Pictures

The ‘pic’ format displays or stores on disk attached pictures. It supports the following options:

-v prog
--viewer prog

Use prog to view images (default: xv).

-d text
--description text

Look for pictures with this descriptive text.

-m type
--mime-type type

Look for pictures with this MIME type.

-s
--store

Store pictures on disk, instead of displaying them. The picture names are created by expanding the file name template, given with the following option:

-f template
--file template

Set the template for output file names (implies –store). The template can contain the following meta-characters:

CharExpands to
~DInput file directory part
~NInput file base name
~CContent description
~TMime type without the ‘image/’ prefix
~PPicture type
~IPID of the idest process

The default template is ‘/tmp/~I-~N.~T’.

-h
--help

Show a short help summary

10.3.2.5 shortlist: Display Short Information

The ‘shortlist’ format module is similar to the ‘shortlist.scm’ example program, discussed in shortlist example. It does not take any command line options – everything after the format name is treated as file names:

$ idest --format=shortlist *.mp3
dnr.mp3: Diamonds & Rust by Joan Baez, 1975
ams.mp3: Amsterdam, by Jacques Brel, 1968

10.4 Batch

Batch modules or batches are idest module files located in a set of predefined directories which apply a set of modifications to the argument files. In other words, batches are file-modifying counterpart of formats. A batch is invoked using the --batch (-B) command line option. The batch name is given as argument to that option. Similarly to the --source and --format options, the --batch option stops further argument processing and passes the rest of arguments to the batch module, which is supposed to remove its option arguments and leave only the input file names. For example:

$ idest --batch=setpic -f cover.png file.mp3

In this example, ‘setpic’ is the batch module name, ‘-f cover.png’ are its arguments (see setpic), and ‘file.mp3’ is the argument file.

10.4.1 How to Write New Batch Modules

The rules for writing batch modules are similar to those for formats (see format modules) with only few differences.

The source for format module name must be saved in the file named name.scm located in the subdirectory idest/batch somewhere in the Guile load path. It must begin with the following clause:

(define-module (idest batch name))

The module must define and export the ‘idest-main’ function, whose calling convention is the same as that in the usual idest scripts (see idest-main). This function must return the new list of frames. If it returns an empty list, all existing frames will be deleted. If the function chooses not to modify any frames, it must return #f.

If the module needs to process command line arguments, it should do so in the function ‘idest-init’, defined as:

(define-public (idest-init)
  ...)

Finally, the module should export the symbol ‘description’ with a concise description of the module. This description will be shown in the --batch=help output (see help batch).

To illustrate this, here is the code for module ‘delfrm’, which removes the requested frames from all argument files:

(define-module (idest batch delfrm))

(define-public description
 "remove requested frames from the input files")

(define frame-list '())

(define-public (idest-main)
  (filter
   (lambda (frame)
     (not (member (car frame) frame-list)))
   frames))

(define-public (idest-init)
  (let ((cmd (command-line)))
    (cond
     ((< (length cmd) 3)
      (error
       "usage: idest --batch=delfrm FRAME-LIST FILE...")
      (exit 1))
     (else
      (set! frame-list (string-split (list-ref cmd 1) #\,))
      (set-program-arguments
        (cons (car cmd) (list-tail cmd 2)))))))

10.4.2 Existing Batch Modules

Idest is shipped with a set of predefined batch modules. These modules are found in the scheme/idest/batch subdirectory of the source tree. They are installed into the version-site-dir’/batch directory (see version-site-dir).

10.4.2.1 help: List and Describe Available Batches

The ‘help’ batch searches the load path for available batch modules and lists them. For each module its name and short description are shown on a separate line. The output is sorted alphabetically by the format name:

$ idest --format=help
setlyrics: set song lyrics (USLT frame) from a file
setpic: set attached picture from a file

If ‘help’ is used with the --which (-w) option, the format includes the directory where the module is found:

$ idest --format=help --which
setlyrics (/usr/share/idest/format): set song lyrics
(USLT frame) from a file
...

10.4.2.2 setlyrics

The ‘setlyrics’ batch reads the text from the specified file (or standard input, if no file is given) and stores it in the ‘USLT’ frame. It supports the following command line options:

-f file
--file file

Read text from file (default: stdin).

-l name
--lang name

Set language in which the lyrics is written, i.e. the value of the ‘lang’ qualifier (default: ‘eng’).

-c text
--content text

Set content description.

-h
--help

Show a short help summary

10.4.2.3 setpic: Attach a Picture

The ‘setpic’ module reads a picture from a supplied file and attaches it to the argument files. It supports the following options:

-f file
--file file

Read picture from file. This option is required.

-d text
--description text

Set the value of ‘condesc’ qualifier.

-m type
--mime-type type

Set MIME type. By default it is deduced from the picture file suffix.

-p num
--pic-type num

Set picture type (a decimal number). Default is ‘0’.

-h
--help

Show a short help summary

For example:

$ idest --batch setpic --file cover.png \
        --description='Album Cover' file.mp3

10.5 Testing Scripts

When writing a script which modifies tags, it is good idea to test it before applying it to your data. Idest provides a special option for that: --dry-run (-n, e.g.:

$ idest --dry-run --script settitle *.mp3

This will run your script as usual, but instead of applying the changes to the input files, idest will verbosely print results of each invocation of ‘idest-main’. When --dry-run is used, input files are opened in read-only mode.

This option works with batch files as well, e.g.:

$ idest --dry-run --batch delfrm *.mp3

Here is an example of the dry-run output, obtained from the command above:

dry-run: loading ../examples/settitle.scm ...
dry-run: loading /usr/share/guile/1.8/ice-9/regex.scm ...
dry-run: loading /usr/share/guile/1.8/srfi/srfi-13.scm ...
File Tinc_un_clavell_per_a_tu.mp3
(TIT2 (text . Tinc un clavell per a tu))
(TALB (descr . Album/movie/show title) (text . Maremar))
...

The first frame shown (‘TIT2’) was produced by settitle.scm (see the previous chapter). Rest of frames come from the input file itself.

Notice the diagnostics lines which start with ‘dry-run’. In dry-run mode idest verbosely reports the full file names of all files it loads. In this particular case, the line

dry-run: loading ../examples/settitle.scm ...

shows the full path of the script file itself, whereas the two lines

dry-run: loading /usr/share/guile/1.8/ice-9/regex.scm ...
dry-run: loading /usr/share/guile/1.8/srfi/srfi-13.scm ...

reflect the use-modules clause at the beginning of settitle.scm (see settitle.scm).

Implementation note

The ‘dry-run’ mode is actually implemented as a usual idest Guile script, named dry-run.scm. The script is installed to the package script directory. Its source can be found in the subdirectory scheme of the idest distribution.

IdEst – ID3 Editing and Scripting Tool (split by chapter):   Section:   Chapter:FastBack: Scripting   Up: Scripting   FastForward: Backups   Contents: Table of ContentsIndex: Concept Index