# ID3 Tags (#136)

**URL:** <https://www.ruby-forum.com/t/id3-tags-136/113406>\
**Category:** Ruby\
**Created:** [August 30, 2007, 8:00pm UTC](https://www.ruby-forum.com/t/id3-tags-136/113406 "2007-08-30T20:00:38Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![bbazzarrakk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/b/47e85d/32.png) [@bbazzarrakk](https://www.ruby-forum.com/u/bbazzarrakk)\
**Post date:** [August 30, 2007, 8:00pm UTC](https://www.ruby-forum.com/t/id3-tags-136/113406/1 "2007-08-30T20:00:38Z")

</div>

This quiz was another idea I got out of the Erlang book. The author  
uses a  
similar example to show how smooth processing binary data in Erlang can  
be. I’m  
happy to say that I found the submitted Ruby solutions to be equally  
smooth, if  
not more so.

The secret to binary parsing in Ruby is generally the String.unpack()  
method and  
the majority of the solutions capitalized on this technique.  
Technically, ID3  
tags are mainly in plain text, with some null characters thrown in.  
Still, I  
think it’s a good idea to get into the unpack() mindset anytime you  
start  
slicing up binary data.

I want to take a look at Eugene K.'s code below. It’s a pretty  
typical  
usage of unpack() to parse some data. It also includes a nicety when  
reading  
the file that I’m ashamed to admit I didn’t think of. Let’s start with  
that:

def fileTail (file, offset)  
f=File.new(file)  
f.seek(-offset,IO::SEEK\_END)  
f.read  
end

# …

In my own code, I read the whole file into memory and indexed out the  
last 128  
bytes. That’s almost always the wrong approach and Eugene shows the  
correct  
strategy above. This code just opens the file, seek()s to offset bytes  
before  
the end, and read()s the needed data. That scales much better when the  
data  
sizes are significant.

As a quick aside, file\_tail() would probably be a more Rubyish method  
name.

The code now builds a data structure class to hold the tag details. It  
starts  
like this:

# …

class ID3Tag  
GENRES=[“Blues”,“Classic Rock”,“Country”,â€¦,“Dance Hall”]  
attr\_reader :title, :artist, :album, :year, :comment, :genre, :track

```
# ...

```

You can see that this class is mainly just a data structure that defines  
readers  
for all of the elements in a tag. I’ve trimmed the GENRES listing here,  
but the  
code included the full set.

I will say that some found more clever means to load the GENRES Array.  
Several  
people did fancy heredoc manipulations, but the most clever pulled the  
list out  
of the quiz document using open-uri and hpricot. That was especially  
wise this  
time since I made so many mistakes in the quiz description.

We’re now ready for the actual parsing code:

```
# ...

def initialize fname
  tag,@title,@artist,@album,@year,@comment,@genre=
    fileTail(fname,128).unpack "A3A30A30A30A4A30C"
  raise "No ID3 Info" if tag!='TAG'
  s_com,flag,track=@comment.unpack "A28CC"
  if flag==0 and track!=0
    @comment=s_com
    @track=track
  end
  @genre=GENRES[@genre]
  @genre="Unknown" if !@genre
end

```

end

# …

As you can see, the majority of the work is done on the first line with  
a single  
call to unpack(). The template fed to unpack() is the key to the whole  
puzzle.  
An “A” in the unpack() template instructs it to extract a String,  
removing any  
trailing spaces or null characters. By default the String is just one  
character  
long, but you can provide a number after the “A” to increase that count.  
The  
only other character used in the template is a “C” which is used to  
extract one  
character as an Integer. The unpack() call returns an Array which  
Eugene just  
mass-assigns to the relevant variables.

The rest is simple. The code checks the first chunk for the identifying  
“TAG”  
String and throws an error if it’s not there. Then another call to  
unpack(),  
with a template much like the first, pulls the track field out of the  
comment.  
The if statement makes sure that assignment only happens when it is  
present.  
The final two lines are just a longhand form of:

@genre = GENRES[@genre] || “Unknown”

With all of the fields stored away in the proper variables, reader calls  
can be  
used to extract as needed. Eugene’s actual application code just punted  
on that  
point though:

# …

p ID3Tag.new(ARGV[0])

My thanks to all who have helped me with my Erlang comparisons these  
last two  
weeks. I promise, we’re on to new topics now.

In fact, tomorrow we will tackle an interesting subproblem from this  
year’s ICFP  
contest…
