Make your own music tagging framework with Groovy

I'll separate the framework I've been using into a separate class and then write a test program to exercise it.
Register or Login to like
music infinity

In this series, I'm developing several scripts to help in cleaning up my music collection. In the last article I wrote and tested a Groovy script to clean up the motley assembly of tag fields. In this article, I'll separate the framework I've been using into a separate class and then write a test program to exercise it.

Install Java and Groovy

Groovy is based on Java and requires a Java installation. Both a recent and decent version of Java and Groovy might be in your Linux distribution's repositories. Groovy can also be installed following the instructions on the Groovy homepage. A nice alternative for Linux users is SDKMan, which can be used to get multiple versions of Java, Groovy and many other related tools. For this article, I'm using SDK's releases of:

  • Java: version 11.0.12-open of OpenJDK 11;
  • Groovy: version 3.0.8.

Back to the problem

If you haven't read parts 1-5 of this series, do that now so you understand the intended structure of my music directory, the framework created in that article and how we pick up FLAC, MP3 and OGG files.

The framework class

As I have mentioned a number of times, because of the music directory structure, we have a standard framework to read the artist subdirectories, the album sub-subdirectories, the music, and other files contained within. Rather than copying that code into each script, you should create a Groovy class that encapsulates the general framework behavior and delegates the application-specific behavior to scripts that call it.

Here's the framework, moved into a Groovy class:

 1    public class TagAnalyzerFramework {
 2        // called before any data is processed
 3        Closure atBeginning
 4        // called for each file to be processed
 5        Closure onEachLine
 6        // called after all data is processed
 7        Closure atEnd
 8        // the full path name to the music library
 9        String musicLibraryDirName
10        public void processMusicLibrary() {
11            // Before we start processing...
12            atBeginning()
13            // Iterate over each dir in music library
14            // These are assumed to be artist directories
15            new File(musicLibraryDirName).eachDir { artistDir ->
16                // Iterate over each dir in artist dir
17                // These are assumed to be album directories
18                artistDir.eachDir { albumDir ->
19                    // Iterate over each file in the album directory
20                    // These are assumed to be content or related
21                    // (cover.jpg, PDFs with liner notes etc)
22                    albumDir.eachFile { contentFile ->
23                        // Then on each line...
24                        onEachLine(artistDir, albumDir, contentFile)
25                    }
26                }
27            }
28            // And before we finish...
29            atEnd()
30        }
31    }

Line 1 introduces the public class name.

Lines 2-7 declare the three closures that the application script uses to define the specifics of the processing needed. This is called delegation of behavior.

Lines 8-9 declare the string holding the music directory file name.

Lines 10-30 declare the method that actually handles the processing.

Line 12 calls the Closure that is run before any data is processed.

Lines 15-27 loop over the artist/album/content file structure.

Line 24 calls the Closure that processes each content file.

Line 29 calls the Closure that is run after all data is processed.

I want to compile this class before I use it, as follows:

$ groovyc TagAnalyzerFramework.groovy$

That's it for the framework.

Using the framework in a script

Here's a simple script that prints out a bar-separated value listing of all the files in the music directory:

 1  int fileCount
 2  def myTagAnalyzer = new TagAnalyzerFramework()
 3  myTagAnalyzer.atBeginning = {
 4      // Print the CSV file header and initialize the file counter
 5      println "artistDir|albumDir|contentFile"
 6      fileCount = 0
 7  }
 8  myTagAnalyzer.onEachLine = { artistDir, albumDir, contentFile ->
 9      // Print the line for this file
10      println "$|$|$"
11      fileCount++
12  }
13  myTagAnalyzer.atEnd = {
14      // Print the file counter value
15      System.err.println "fileCount $fileCount"
16  }
17  myTagAnalyzer.musicLibraryDirName = '/home/clh/Test/Music'
18  myTagAnalyzer.processMusicLibrary()

Line 1 defines a local variable, fileCount, used to count the number of content files. Note that this variable doesn't need to be final.

Line 2 calls the constructor for the TagAnalyzerFramework class.

Line 3 does what looks like a mistake in Java. It appears to refer to a field in a foreign class. However, in Groovy this is actually calling a setter on that field, so it's acceptable, as long as the implementing class "remembers" that it has a contract to supply a setter for this property.

Lines 3-7 create a Closure that prints the bar-separated value header and initialize the fileCount variable.

Lines 8-12 similarly define the Closure that handles the logic for processing each line. In this case,it is simply printing the artist, album and content file names. If I refer back to line 24 of TagAnalyzerFramework, I see that it calls this Closure with three arguments corresponding to the parameters shown here.

Lines 13-16 define the Closure that wraps up the processing once all the data is read. In this case, it prints a count of files to standard error.

Line 17 sets the music library directory name.

And line 18 calls the method to process the music library.

Run the script:

$ groovy MyTagAnalyzer.groovy
St Germain|Tourist|04_-_St Germain_-_Land Of....flac
fileCount 55

Of course the .class files created by compiling the framework class must be on the classpath for this to work. Naturally, I could use jar to package up those class files.

Those who are made queasy by what looks like setting fields in a foreign class could define local instances of closures and pass those as parameters, either to the constructor or processMusicLibrary(), and achieve the same effect.

I could go back to the code samples provided in the earlier articles to retrofit this framework class. I'll leave that exercise to the reader.

Delegation of behavior

To me, the coolest thing happening here is the delegation of behavior, which requires various shenanigans in other languages. For many years, Java required anonymous classes and quite a bit of extra code. Lambdas have gone a long way to fixing this, but they still cannot refer to non-final variables outside their scope.

That's it for this series on using Groovy to manage the tags in my music library. There will be more Groovy articles in the future.

What to read next
Chris Hermansen portrait Temuco Chile
Seldom without a computer of some sort since graduating from the University of British Columbia in 1978, I have been a full-time Linux user since 2005, a full-time Solaris and SunOS user from 1986 through 2005, and UNIX System V user before that.
Creative Commons LicenseThis work is licensed under a Creative Commons Attribution-Share Alike 4.0 International License.