Ꮬ deKonvoluted Projects Archives About

This is part of a series of posts.

Code relevant to this post is available at this repository.

I like to think of scripts from the inside out. In the previous update, I got the core functionality in place. The FlacFile class can handle any given FLAC file and re-encode and normalize it. The next layer of the script deals with processing an input directory.

This can just be a simple function that accepts a path and recurses through the directory attempting to find FLAC files inside it. To avoid infinite loops and other messy affairs, we will not follow any symbolic links and stick to true directories. Such a function can be written like this,

1
2
3
4
5
6
7
8
9
10
11
12
13
def processDir( dirPath )
    return if not File.directory?( dirPath )

    Dir.foreach( dirPath ) { |content|
        contentPath = dirPath + "/" + content
        if File.directory?( contentPath )
            processDir contentPath
        elseif File.file?( contentPath )
            flacFile = FlacFile.new contentPath
            flacFile.normalize
        end
    }
end

We are assuming that this function only gets passed valid dirPaths. Still, for sanity sake, we’ll test to make sure it’s a valid directory (or symlink, to be complete). Next, we iterate over each entry in the directory. If the entry/content is a directory, we recurse (opening us to a logic issue of potentially following a symlink). And if the content is a file, we’ll try to normalize it.

First, we need to close the logic gap of potentially following unsafe symlinks.

1
2
contentPath = dirPath + "/" + content
next if File.symlink?( contentPath )

Next, the foreach block ends up following the . and .. directories as well, causing trouble. To fix that, we’ll skip those.

1
2
Dir.foreach( dirPath ) { |content|
    next if content == "." or content == ".."

Awesome. Now, it would be great to fork off normalization processes off the parent thread. That way, all FLAC files in the same directory could be processed at once. Typically, we expect a dozen FLAC files to be present in a directory, so this should not get too out of hand. If this assumption is not true and you have a folder with dozens (or worse, hundreds) of FLAC files, this is potentially unsafe and will use a lot of resources. Here’s the original block:

1
2
3
4
5
6
7
8
9
10
11
12
13
Dir.foreach( dirPath ) { |content|
    next if content == "." or content == ".."

    contentPath = dirPath + "/" + content
    next if File.symlink?( contentPath )

    if File.directory?( contentPath )
        processDir( contentPath )
    elsif
        flacFile = FlacFile.new contentPath
        flacFile.normalize
    end
}

And here’s how the forked version looks. Notice that we wait for all the forked processes to finish at each level before moving on.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Dir.foreach( dirPath ) { |content|
    next if content == "." or content == ".."

    contentPath = dirPath + "/" + content
    next if File.symlink?( contentPath )

    if File.directory?( contentPath )
        processDir( contentPath )
    elsif
        fork do
            flacFile = FlacFile.new contentPath
            flacFile.normalize
        end
    end
}

Process.wait

This function is done good to go. As long as it’s given a valid, absolute directory path, it will merrily recurse and process all files and directories inside. If any FLAC files are found, it will attempt to normalize them, else it will leave them alone.

EDIT: As it turned out, this function was actually NOT good to go. Read the next update to checkout the solution.

Great. Now, it’s time to move on to the next outer layer. This function must take a raw input parameter and figure out if it’s a valid argument. If the directory or file doesn’t exist, the user must be told of that. I decided to not stop the script in that case, so the script will just print an error message for an incorrect path and move on to the other arguments.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def process( input )
    if not File.exists?( input )
        puts "ERROR. #{input} not found."
        return
    end

    inputPath = File.absolute_path( input )

    if File.file?( input )
        flacFile = FlacFile.new inputPath
        flacFile.normalize
    elsif File.directory?( inputPath )
        if not File.symlink?( inputPath )
            processDir inputPath
        end
    end
end

That works. Finally, the outer-most layer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require 'optparse'

if __FILE__ == $0
    optparser = OptionParser.new do |opts|
        opts.banner = "Usage: #{$0} [-h|--help] [FILE|DIR] [FILE|DIR] ..."

        opts.on( "-h", "--help", '''Display this help message.

This script will reencode FLAC files and apply replay gain normalization...

        #... snipped ...

        ''' ) do
            puts opts
            exit
        end
    end

    optparser.parse!

    ARGV.each do |input|
        process input
    end
end

That’s basically it. The option parser handles printing out the help message. I’m still not too happy with the formatting of the help message, but for now, it’s in good shape. After calling parse! on the option parser, ARGV will only contain the arguments. We can just loop over them and process each in turn.

This script can now do everything the bash script could do, while able to handle short and long options, files and directories or a mix of both, with and without spaces in them.

Next steps

One thing I would like to improve is to take in both process and processDir into the FlacFile class (perhaps renaming it to something like Normalizer or Reencoder). That way, the outer loop just sifts through inputs and feeds absolute paths (when available) to the class and the class goes off and does the rest. This way, vulnerable functions like processDir can be put in private access and hidden away.

For now, I’ll clean up the code a bit and tag a new release. The repository is, of course, here. Feel free to use, modify and pass on.


Scroll to top

© 2018 Karthik Periagaram