Mat's Playground

music.applyTracklist

Description

This script is called with directories as parameter music.applyTracklist dir_1 dir_2 .. dir_n. In every directory it takes the content of the file "index" and writes the track information it finds therein into the music (mp3 or ogg) files. It will also rename the files so that the filename reflects the song title.

For the sake of clarity, here's an index file:

Artist: Killswitch Engage
Title:  The End of Heartache
Genre:  Metal
Year:   2004

01: A Bid Farewell
02: Take This Oath
03: When Darkness Falls
04: Rose Of Sharyn
05: Inhale
06: Breathe Life
07: The End Of Heartache
08: Declaration
09: World Ablaze
10: And Embers Rise
11: Wasted Sacrifice
12: Hope Is ...

For each track it expects to find a file called track(nr).cdda.ogg or track(nr).cdda.wav.mp3. If one of the files is missing, or not every tag (e.g. the artist) is specified, the script will abort before changing any file.

Note: The cd.makeOgg will create files compatible to this scheme, so you don't need to worry.

When all necesarry information is available, all tags are written into the files and they will be renamed to (nr)_(title).(ogg,mp3)


The script also knows the parameter --update. If you use that (e.g. music.applyTracklist --update dir_1), it will use just the first two characters of each filename to find the tracks.

This way you can quickly rename files like 06-Track06.ogg to 06_breathe_life.ogg

In Update Mode, only those tracks will be touched, whose tags are really different from those in the index file.


If some (or all) artists of a song differ from the "global" album artist, you can specify him as well by enclosing him in ## as can be seen in this example:

Artist: Various
Title:  Matrix
Genre:  Soundtrack
Year:   1999

01: ##Marilyn Manson##   Rock Is Dead
02: ##Propellerheads##   Spybreak! (short one)
03: ##Ministry##         Bad Blood
..

In this case the files will be renamed according to the scheme (nr)_(artist)_(title).(mp3,ogg).


Note1: The script does only support track numbers up to 99 and will not check for higher numbers.

Note2: For ogg files, the genre can be an arbitrary string. If you use mp3 files, your choice is somewhat limited. In that case, use id3v2 -L to see a list of valid options.

Required programs and scripts

Download

Source code

Hide line numbers Expand lines
  1#!/bin/sh
  2# the next line restarts using tclsh \
  3exec tclsh "$0" "$@"
  4
  5#
  6# needed programs:
  7#  id3v2
  8#  metaflac
  9#  ogg.displayTags (script)
 10#  ogg.writeTags (script)
 11#
 12
 13
 14#
 15# do we do an update?
 16#
 17set do_update no
 18set i [lsearch $argv "--update"]
 19if {$i > -1} then {
 20    # we do an update
 21    set do_update yes
 22    set argv [lreplace $argv $i $i]
 23}
 24unset i
 25
 26
 27#
 28# exit if nothing to do
 29#
 30if {[llength $argv] == 0} then {
 31    puts "Usage: [file tail $argv0] \[option\] \[directory..\]\n"
 32
 33    puts "\t--update\tupdate existing files\n"
 34    exit 1
 35}
 36
 37
 38
 39#
 40# prepare the list of genres (mp3 files need number instead of plain names)
 41#
 42
 43#read genre list
 44set genre_list [exec id3v2 -L]
 45
 46# transform into tcl list
 47regsub -all ": " $genre_list " \"" genre_list
 48regsub -all "\n" $genre_list "\"\}\n\{" genre_list
 49
 50set genre_list "\{$genre_list\"\}"
 51
 52# this function finds the corresponding number to a genre
 53proc genre_nr {val} {
 54    global genre_list
 55
 56    set return "-1"
 57
 58    foreach item $genre_list {
 59        set nr   [lindex $item 0]
 60        set name [lindex $item 1]
 61
 62        if {[string compare -nocase $name $val] == 0} then {
 63            set return $nr
 64            unset nr name
 65            break
 66        }
 67
 68        unset nr name
 69    }
 70
 71    return $return
 72}
 73
 74
 75#
 76# First, check for correct tracklists
 77#
 78
 79set errors "no"
 80
 81
 82foreach folder [lsort -dictionary $argv] {
 83    set folder [file normalize $folder]
 84
 85    if {![file isdirectory $folder]} then {
 86        unset folder
 87        continue
 88    }
 89
 90    #
 91    # check for missing tracklists
 92    #
 93    set tracklist_file [file join $folder index]
 94
 95    if {![file exists $tracklist_file]} then {
 96        puts "no tracklist in $folder"
 97
 98        set errors yes
 99        unset tracklist_file
100        continue
101    }
102
103    #
104    # check, whether there's a tracklist entry for each file
105    #
106
107    # reads tracklist into an array
108
109    array set contents {}
110
111    set fd [open $tracklist_file r]
112    while {![eof $fd]} {
113        set line [split [gets $fd] :]
114
115        if {[llength $line] >= 2} then {
116            set name  [string trim [lindex $line 0]]
117            set value [string trim [join [lrange $line 1 end] ":"]]
118
119            array set contents [list $name $value]
120            unset name value
121        }
122        unset line
123    }
124
125    close $fd
126    unset fd tracklist_file
127
128
129    # does an album title exist
130    if {[array get contents Title] == ""} then {
131        puts "tracklist in $folder has no title"
132        array unset contents
133        unset folder
134
135        set errors yes
136        continue
137    }
138
139    # does an artist exist
140    if {[array get contents Artist] == ""} then {
141        puts "tracklist in $folder has no artist"
142        array unset contents
143        unset folder
144
145        set errors yes
146        continue
147    }
148
149    # does an genre exist
150    if {[array get contents Genre] == ""} then {
151        puts "tracklist in $folder has no genre"
152        array unset contents
153        unset folder
154
155        set errors yes
156        continue
157    }
158
159    # is this genre an valid one
160    # (only necessary if there are mp3 files in this directory, since
161    #  oggs can have arbitrary genres)
162    if {[glob -nocomplain -types f [file join $folder *.mp3]] != ""} then {
163        if {[genre_nr $contents(Genre)] == "-1"} then {
164            puts "tracklist in $folder uses unknown genre"
165            array unset contents
166            unset folder
167
168            set errors yes
169            continue
170        }
171    }
172
173    # does an year exist
174    if {[array get contents Year] == ""} then {
175        puts "tracklist in $folder has no year"
176        array unset contents
177        unset folder
178
179        set errors yes
180        continue
181    }
182
183    # exists a title for all tracks?
184    if {$do_update} then {
185        foreach file [lsort [glob -nocomplain -types f [file join $folder *.{ogg,mp3,flac}]]] {
186
187            # file is '$folder/07*.ogg', we only need the track nr
188            set nr [string range [file tail $file] 0 1]
189
190            if {[array get contents $nr] == ""} then {
191                puts "[file tail $file] in $folder has no title"
192                set errors yes
193            }
194            unset nr file
195        }
196    } else {
197        foreach file [lsort [glob -nocomplain -types f [file join $folder *.cdda.{wav.mp3,ogg,flac}]]] {
198            # file is '$folder/track07.cdda.ogg', we only need the track nr
199            set nr [string range [file tail $file] 5 6]
200
201            if {[array get contents $nr] == ""} then {
202                puts "[file tail $file] in $folder has no title"
203                set errors yes
204            }
205            unset nr file
206        }
207    }
208
209    # exists a title in the tracklist, but no such file?
210    foreach nr [lsort [array names contents]] {
211        if {$nr == "Title" || $nr == "Artist" || $nr == "Genre" || $nr == "Year"} then {unset nr; continue}
212
213        if {$do_update} then {
214            if {[glob -nocomplain -types f [file join $folder "[set nr]{_,-, }*.{ogg,mp3,flac}"]] == ""} then {
215                puts "no file for track $nr in $folder"
216                set errors yes
217            }
218        } else {
219            if {[glob -nocomplain -types f [file join $folder "track[set nr].cdda.{ogg,wav.mp3,flac}"]] == ""} then {
220                puts "no file for track $nr in $folder"
221                set errors yes
222            }
223        }
224        unset nr
225    }
226
227
228    array unset contents
229    unset folder
230}
231
232if {$errors} then {unset errors; exit}
233unset errors
234
235
236###########################################
237# Tracklists are OK, now rename the files #
238###########################################
239
240foreach folder [lsort -dictionary $argv] {
241    set folder [file normalize $folder]
242
243    if {![file isdirectory $folder]} then {
244        unset folder
245        continue
246    }
247
248    puts "$folder/"
249
250    # set tracklist
251    set tracklist_file [file join $folder index]
252
253    # reads tracklist into an array
254    array set contents {}
255
256    set fd [open $tracklist_file r]
257    while {![eof $fd]} {
258        set line [split [gets $fd] :]
259
260        if {[llength $line] >= 2} then {
261            set name  [string trim [lindex $line 0]]
262            set value [string trim [join [lrange $line 1 end] ":"]]
263
264            array set contents [list $name $value]
265            unset name value
266        }
267        unset line
268    }
269
270    close $fd
271    unset fd tracklist_file
272
273
274    # rename files
275    if {$do_update} then {
276        set l [lsort [glob -nocomplain -types f [file join $folder *.{ogg,mp3,flac}]]]
277    } else {
278        set l [lsort [glob -nocomplain -types f [file join $folder *.cdda.{wav.mp3,ogg,flac}]]]
279    }
280
281    foreach oldfile $l {
282        if {$do_update} then {
283            # file is '$folder/07*.ogg', we only need the track nr
284            set nr [string range [file tail $oldfile] 0 1]
285        } else {
286            # file is '$folder/track07.cdda.ogg', we only need the track nr
287            set nr [string range [file tail $oldfile] 5 6]
288        }
289
290        set artist [lindex [array get contents Artist] 1]
291        set album  [lindex [array get contents Title]  1]
292        set genre  [lindex [array get contents Genre]  1]
293        set year   [lindex [array get contents Year]   1]
294        set title  [lindex [array get contents $nr]    1]
295
296        # each track can have it's own artist (e.g. "01: ##Creed## Are You Ready?")
297        # here we need to extract it
298        set alternate_artist no
299        if {[string range $title 0 1] == "##"} then {
300            set artist [lindex [split $title "#"] 2]
301            set title  [string trim [join [lrange [split $title "#"] 4 end] "#"]]
302            set alternate_artist yes
303        }
304
305        set ext [file extension $oldfile]
306
307        # get old tag information
308        if {$ext == ".ogg"} then {
309            set old_infos  [exec ogg.displayTags $oldfile]
310
311            set old_album  ""
312            set old_artist ""
313            set old_year   ""
314            set old_genre  ""
315            set old_title  ""
316            set old_nr     ""
317
318            foreach line [split $old_infos \n] {
319                set line [split $line =]
320                set tag  [string trim [lindex $line 0]]
321                set val  [string trim [join [lrange $line 1 end] =]]
322
323                switch $tag {
324                    "ALBUM" {
325                        set old_album $val
326                    }
327                    "ARTIST" {
328                        set old_artist $val
329                    }
330                    "DATE" {
331                        set old_year $val
332                    }
333                    "GENRE" {
334                        set old_genre $val
335                    }
336                    "TITLE" {
337                        set old_title $val
338                    }
339                    "TRACKNR" {
340                        set old_nr $val
341                    }
342                }
343                unset line tag val
344            }
345            unset old_infos
346
347        } elseif {$ext == ".flac"} then {
348            set old_album  [join [lrange [split [exec metaflac --show-tag=ALBUM $oldfile] =] 1 end]]
349            set old_artist [join [lrange [split [exec metaflac --show-tag=ARTIST $oldfile] =] 1 end]]
350            set old_year   [join [lrange [split [exec metaflac --show-tag=DATE $oldfile] =] 1 end]]
351            set old_genre  [join [lrange [split [exec metaflac --show-tag=GENRE $oldfile] =] 1 end]]
352            set old_title  [join [lrange [split [exec metaflac --show-tag=TITLE $oldfile] =] 1 end]]
353            set old_nr     [join [lrange [split [exec metaflac --show-tag=TRACKNUMBER $oldfile] =] 1 end]]
354        } else {
355            set old_infos [exec id3v2 -l $oldfile]
356
357            set old_album  ""
358            set old_artist ""
359            set old_year   ""
360            set old_genre  ""
361            set old_title  ""
362            set old_nr     ""
363
364            foreach line [split $old_infos \n] {
365                if {[string range $line 0 3] == "TALB"} then {set old_album  [string trim [join [lrange [split $line ":"] 1 end] ":"]]}
366                if {[string range $line 0 3] == "TPE1"} then {set old_artist [string trim [join [lrange [split $line ":"] 1 end] ":"]]}
367                if {[string range $line 0 3] == "TYER"} then {set old_year   [string trim [join [lrange [split $line ":"] 1 end] ":"]]}
368                if {[string range $line 0 3] == "TCON"} then {set old_genre  [string trim [join [lrange [split $line ":"] 1 end] ":"]]}
369                if {[string range $line 0 3] == "TIT2"} then {set old_title  [string trim [join [lrange [split $line ":"] 1 end] ":"]]}
370                if {[string range $line 0 3] == "TRCK"} then {set old_nr     [string trim [join [lrange [split $line ":"] 1 end] ":"]]}
371                unset line
372            }
373
374            # genre is of format "genre name (nr)".. remove the nr
375            set old_genre [string trim [lindex [split $old_genre (] 0]]
376
377            unset old_infos
378        }
379
380
381        # if tags are the same, we can stop here
382        if {[string match $old_album  $album]  && \
383            [string match $old_artist $artist] && \
384            [string match $old_year   $year]   && \
385            [string match $old_genre  $genre]  && \
386            [string match $old_title  $title]  && \
387            [string match $old_nr     $nr]} then {
388
389            unset old_album old_artist old_year old_genre old_title old_nr
390            unset nr artist album genre year title alternate_artist oldfile ext
391            continue
392        }
393
394
395        # set new tags, if they differ
396        if {$ext == ".ogg"} then {
397            # set OGG tags
398
399            # be sure to escape quotation marks in tags (otherwise ogg.writeTags would choke)
400            foreach var {title artist album} {
401                regsub -all {\"} [set $var] {\\"}   $var
402                unset var
403            }
404
405            # write the tags
406            exec ogg.writeTags --album $album --artist $artist --date $year --genre $genre --title $title --tracknr $nr $oldfile
407
408            # restore the escaped quotation marks again
409            foreach var {title artist album} {
410                regsub -all {\\"} [set $var] "\""   $var
411                unset var
412            }
413
414        } elseif {$ext == ".flac"} then {
415           # delete all existing tags
416           exec metaflac --remove-all-tags $oldfile
417
418           # set tags
419           exec metaflac --set-tag=ALBUM=$album --set-tag=ARTIST=$artist --set-tag=DATE=$year --set-tag=GENRE=$genre --set-tag=TITLE=$title --set-tag=TRACKNUMBER=$nr $oldfile
420
421        } else {
422            # delete all exisiting tags
423            exec id3v2 -D $oldfile
424
425            # set ID3 tags
426            exec id3v2 -a $artist -A $album -t $title -T $nr $oldfile -y $year -g [genre_nr $genre]
427        }
428
429        # prepare title and artist for the new filename
430        foreach var {title artist album} {
431            regsub -all {\.} [set $var] ""   $var
432            regsub -all {\"} [set $var] ""   $var
433            regsub -all {'}  [set $var] ""   $var
434            regsub -all {\?} [set $var] ""   $var
435            regsub -all {,}  [set $var] ""   $var
436            regsub -all {!}  [set $var] ""   $var
437            regsub -all {\;} [set $var] ""   $var
438            regsub -all {\(} [set $var] ""   $var
439            regsub -all {\)} [set $var] ""   $var
440
441            regsub -all {\*} [set $var] " "  $var
442            regsub -all {:}  [set $var] " "  $var
443            regsub -all {#}  [set $var] " "  $var
444            regsub -all {@}  [set $var] " "  $var
445
446            regsub -all {<}  [set $var] "-"  $var
447            regsub -all {>}  [set $var] "-"  $var
448            regsub -all {/}  [set $var] "-"  $var
449
450            regsub -all {Ä}  [set $var] "A" $var
451            regsub -all {ä}  [set $var] "a" $var
452            regsub -all {Ö}  [set $var] "O" $var
453            regsub -all {ö}  [set $var] "o" $var
454            regsub -all {Ü}  [set $var] "U" $var
455            regsub -all {ü}  [set $var] "u" $var
456            regsub -all {ß}  [set $var] "ss" $var
457
458            regsub -all {&}  [set $var] "and" $var
459            regsub -all {\+} [set $var] "and" $var
460            regsub -all { }  [set $var] "_"   $var
461
462            set $var [string tolower [set $var]]
463            unset var
464        }
465
466        # rename the file
467        if {$alternate_artist} then {
468            # alternate artist should appear in filename too
469            set newfile [file join $folder "[set nr]_[set artist]-[set title][set ext]"]
470        } else {
471            set newfile [file join $folder "[set nr]_[set title][set ext]"]
472        }
473
474        if {![string match $oldfile $newfile]} then {
475            file rename -force $oldfile $newfile
476        }
477
478        puts "  [file tail $newfile]"
479
480        unset old_album old_artist old_year old_genre old_title old_nr
481        unset nr artist album genre year title alternate_artist oldfile newfile ext
482    }
483    unset l
484
485    array unset contents
486    unset folder
487}
488
489unset do_update
490
491exit 0