Lab Notes

Things I want to remember how to do.

Ruby Play List Copier, Take 2

August 6, 2012

I finally got back to my little Ruby project over the weekend. The idea was to write a tool to copy an m3u play list and associated files to my mp3 player since Rhythmbox and Banshee were not up to the task. I used the ruby-taglib library from http://robinst.github.com/taglib-ruby/ to access mp3 tags.

My first attempt was turning out a little too much like an enterprisey Java project so I decided to back up and try to make it a little lighter and more Ruby-esque. I decided on a module for parsing play lists would allow for the best re-use for that functionality while simple classes would represent play lists and play list entries. With the library written the main script became the following:

#!/usr/bin/env ruby
#
require 'fileutils'
require './playlist-parser'

dest_dir = File::expand_path(ARGV[0])
source = PlayList.new(ARGV[1])
dest = PlayList.new(File::join(dest_dir, File::basename(source.to_s)))

source.read_playlist do |entry|
basename = PlayListEntry::sanitize(entry.artist + ' - ' + entry.album +
' - ' + entry.track + ' - ' + entry.title + '.mp3')
dest_entry = PlayListEntry.new(basename)
dest.playlist_entries << dest_entry
dest_file = File::join(dest_dir, dest_entry.to_s)
if not File::exists?(dest_file) then
puts "#{entry.source} => #{dest_file}"
FileUtils.copy_file(entry.source, dest_file)
else
puts "#{dest_file} exists"
end
end

dest.write_playlist

This script is pretty simple. It opens the given play list and iterates over the entries, creating a new play list based on the passed destination directory. The file is copied over as Rhythmbox and Banshee do, using the tag information to determine the file name. Then when we are done we write out the new play list.

The library file is little longer. It includes a module named PlayListParser which had the parsing functionality (such as it is, a play list file is not really very complicated; if you are reading this far open one up in a text editor and you’ll figure it out no problem). Then we have the PlayList class which includes the parser module and provides a write_playlist method. Finally the PlayListEntry which makes tag access convenient.

# http://robinst.github.com/taglib-ruby/
require 'taglib'

module PlayListParser

attr_accessor :playlist, :playlist_entries

def parse_playlist(playlist, &block)
@playlist = playlist
@playlist_entries = []
save_dir = Dir::pwd
Dir::chdir(File::dirname(playlist))
File.open(playlist) do |file|
file.readlines.each do |line|
line = line.strip
if line.empty? or line[0] == '#' then
next
end
if not File.exists?(line) then
puts "WARN: File #{line} does not exist in play list #{@playlist}"
end
entry = PlayListEntry.new(line)
@playlist_entries << entry
block.call(entry) unless block == nil
end
end
Dir::chdir(save_dir)
end

end

class PlayList

include PlayListParser

def initialize(playlist)
@playlist = playlist
@playlist_entries = []
end

def read_playlist(&block)
parse_playlist(playlist, &block)
end

def write_playlist
File::open(playlist, 'w') do |file|
file.puts('#EXTM3U')
file.puts(@playlist_entries)
end
end

def to_s
return @playlist.to_s
end
end

class PlayListEntry

def self.pad_track(track)
return ( track < 10 ? '0' + track.to_s : track.to_s)
end

def self.sanitize(source)
return source.gsub(/[":\?]/, '_')
end

attr_accessor :source, :album, :artist, :comment, :genre, :title, :track, :year

def initialize(source)
@source = source
read_tags
end

def read_tags
if File.exists?(source) then
TagLib::FileRef.open(source) do |fileref|
tag = fileref.tag
@album = tag.album
@artist = tag.artist
@comment = tag.comment
@genre = tag.genre
@title = tag.title
@track = PlayListEntry::pad_track(tag.track)
@year = tag.year unless tag.year == 0
end
end
end

def to_s
return @source.to_s
end
end

One drawback of the parser is the use of the current working directory to handle relative paths in the play list file. This construct makes the parse_playlist method not thread-safe. (I can’t help but think about these things after working on servers; but I left it that way since this is supposed to be a simple script.)

In the end I learned a few useful things along the way, like the difference between sub and gsub as well as some of the characters that are escaped by Rhythmbox and Banshee when making file names. Also how to split up a Ruby project into more than one file. And I ended up with something I can actually use. All in all a successful excursion into Ruby.