Friday, June 27, 2008

Ruby for mp3 file organizing

So there we are, me and my friend Oliver, caught in a business trip. We're already bored of the only decent pub in the village, our families are a long distance away, so what's a developer to do? Code in a programming language he/she's not allowed on the job, of course! Oliver seemed interested in Ruby and I've already done a couple of small scripts with it, so we were curious to see what the fuss is about.

It goes without saying that if you want to learn to program (in a particular language) you should not rely too much on books. The only way is to find a task you want to have automated, and then code it using your language of choice. Surely, you must pick the task (or the language) carefully, since not all languages are suitable for all tasks.

One of the things which Oliver has struggled with was organizing all of his podcasts in his player, sorted neatly by directories of author and title. Having found both a hammer and a nail, we were ready to start pounding.

After a bit of research we found the mp3info and id3tag Ruby libraries. id3tag had different fields for ID3v1 and ID3v2 data and didn't have write support (not that we needed it). mp3info didn't have ID3v2.2 support, but I found an interesting link about ID3 internals- the format of the fields was something that could be useful.

After a while our pair programming session has reached a milestone- our script works. It doesn't seem very modular though, so we spend some time making classes and discussing what is the responsibility of each class. Should there be a manager-class? Or should the objects manage themselves? I go with the second approach, and here's the result:


# Class for handling information of the mp3 file
class Mp3File
attr_reader :title, :artist, :album

def initialize filename
@artist = @album = "unknown"
@filename = filename
@title = File.basename(filename, ".mp3")

read_attributes
end

def title
sanitize(@title)
if @title == "unknown" then @title = File.basename(@filename, ".mp3") end
@title
end

def read_attributes
begin
Mp3Info.open(@filename) do |mp3info|
(@title, @artist, @album) = %w{title artist album}.collect { |attrib|
begin
(result = mp3info.tag.send(attrib)).empty? ? "unknown" : result
rescue
"unknown"
end
}
end
rescue
end
end

def sanitize str
str.tr_s!("?'","_")
end

def transfer(newPath)
newPath = eval('"' + newPath + '"')
FileUtils.mkdir_p File.dirname(newPath)
FileUtils.cp @filename, newPath
end
end


This is the class which is initialized with the location of the file and then extracts information about the artist, title and track name. The read_attributes method is meant to show off our new knowledge about the dynamic nature of Ruby- we build a list of methods to invoke on the Mp3Info object, and if no meaningful result, return "unknown". Finally, as the class knows about the current location and mp3 meta-info, it has a method for copying the file to a new location. The new path is passed as a template, where the #@artist, #@album, #@title are substituted with the value of these fields.


class Mp3List

attr_reader :files

def files
@files.map {|file| Mp3File.new(file) }
end

def initialize(sourcePath, days = 7)
@sourcePath = sourcePath
@days = days
@files = read_new
end

def read_new
Dir["#@sourcePath/**/*.mp3"].find_all do |path|
test(?M, path) > (Time.now - (@days * 60 * 60 * 24))
end
end

def to_s
@files.inspect
end
end


Here comes the class, which represents a list of mp3 files in a certain directory (and subdirectories), which satisfies some criteria- in this case, how long ago the files were created (modified). Could it be made more general? Certainly, but in a 80-line script? Maybe next time.


list = Mp3List.new("/home/whoami/Music", 730)
list.files.each do |mp3|
#~ puts "Processing #{filename}"
mp3.transfer('/tmp/music/#@artist/#@album/#@title.mp3')
end


What's left was an example of how to use these classes. Seems good to me- and best of all, it works.

The only thing left was to prepare a patch for the mp3info library for ID3v2.2 support. I actually implemented one (still not incorporated in base), and it also initializes the common fields with either the v2 or v1 data, whatever present (v2 still has precedence, if both are present).

Conclusions from our short session:

  • Ruby is neat for quick hack jobs

  • mp3info does not provide an exhaustive ID3 handling support, but is good enough and workable

  • Pair programming might not be smooth from the start, but you will learn a lot about yourself

  • Organizing your music can sometimes take longer than total time spent looking for your tracks

  • You should choose your business trip accomodation place carefully if you can

1 comment:

Anonymous said...

Those were the days my friend:) Still doing some Ruby development? I didn't find the time and look more into Python or Groovy.

Cheers, Oliver