Ruby Forum Ruby > ID3 Tags (#136)

Posted by James Gray (bbazzarrakk)
on 24.08.2007 14:35
(Received via mailing list)
The three rules of Ruby Quiz:

1.  Please do not post any solutions or spoiler discussion for this quiz 
until
48 hours have passed from the time on this message.

2.  Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3.  Enjoy!

Suggestion:  A [QUIZ] in the subject of emails about the problem helps 
everyone
on Ruby Talk follow the discussion.  Please reply to the original quiz 
message,
if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

The MP3 file format, didn't provide any means for including metadata 
about the
song.  ID3 tags were invented to solve this problem.

You can tell if an MP3 file includes ID3 tags by examining the last 128 
bytes of
the file.  If they begin with the characters TAG, you have found an ID3 
tag.
The format of the tag is as follows:

  TAG song album artist comment year genre

The spaces above are just for us humans.  The actual tags are 
fixed-width fields
with no spacing between them.  Song, album, artist, and comment are 30 
bytes
each.  The year is four bytes and the genre just gets one, which is an 
index
into a list of predefined genres I'll include at the end of this quiz.

A minor change was later made to ID3 tags to allow them to include track
numbers, creating ID3v1.1.  In that format, if the 29th byte of a 
comment is
null and the 30th is not, the 30th byte is an integer representing the 
track
number.

Later changes evolved ID3v2 which is a scary beast we won't worry about.

This week's Ruby Quiz is to write an ID3 tag parser.  Using a library is
cheating.  Roll up your sleeves and parse it yourself.  It's not hard at 
all.

If you don't have MP3 files to test your solution on, you can find some 
free
files at:

  http://www.mfiles.co.uk/mp3-files.htm

Here's the official genre list with some extensions added by Winamp:

  Blues
  Classic Rock
  Country
  Dance
  Disco
  Funk
  Grunge
  Hip-Hop
  Jazz
  Metal
  New Age
  Oldies
  Other
  Pop
  R&B
  Rap
  Reggae
  Rock
  Techno
  Industrial
  Alternative
  Ska
  Death Metal
  Pranks
  Soundtrack
  Euro-Techno
  Ambient
  Trip-Hop
  Vocal
  Jazz+Funk
  Fusion
  Trance
  Classical
  Instrumental
  Acid
  House
  Game
  Sound Clip
  Gospel
  Noise
  AlternRock
  Bass
  Soul
  Punk
  Space
  Meditative
  Instrumental Pop
  Instrumental Rock
  Ethnic
  Gothic
  Darkwave
  Techno-Industrial
  Electronic
  Pop-Folk
  Eurodance
  Dream
  Southern Rock
  Comedy
  Cult
  Gangsta
  Top 40
  Christian Rap
  Pop/Funk
  Jungle
  Native American
  Cabaret
  New Wave
  Psychadelic
  Rave
  Showtunes
  Trailer
  Lo-Fi
  Tribal
  Acid Punk
  Acid Jazz
  Polka
  Retro
  Musical
  Rock & Roll
  Hard Rock
  Folk
  Folk-Rock
  National Folk
  Swing
  Fast Fusion
  Bebob
  Latin
  Revival
  Celtic
  Bluegrass
  Avantgarde
  Gothic Rock
  Progressive Rock
  Psychedelic Rock
  Symphonic Rock
  Slow Rock
  Big Band
  Chorus
  Easy Listening
  Acoustic
  Humour
  Speech
  Chanson
  Opera
  Chamber Music
  Sonata
  Symphony
  Booty Bass
  Primus
  Porn Groove
  Satire
  Slow Jam
  Club
  Tango
  Samba
  Folklore
  Ballad
  Power Ballad
  Rhythmic Soul
  Freestyle
  Duet
  Punk Rock
  Drum Solo
  A capella
  Euro-House
  Dance Hall
Posted by Robert Dober (Guest)
on 24.08.2007 14:48
(Received via mailing list)
On 8/24/07, Ruby Quiz <james@grayproductions.net> wrote:
> The three rules of Ruby Quiz:
<snip>
> The spaces above are just for us humans.  The actual tags are fixed-width fields
> with no spacing between them.  Song, album, artist, and comment are 30 bytes
> each.  The year is four bytes and the genre just gets one, which is an index
> into a list of predefined genres I'll include at the end of this quiz.
zero based, I guess?
<snip>

Cheers
Robert
Posted by James Gray (bbazzarrakk)
on 24.08.2007 15:54
(Received via mailing list)
On Aug 24, 2007, at 7:47 AM, Robert Dober wrote:

>> quiz.
> zero based, I guess?

It is, yes.

James Edward Gray II
Posted by Cédric Finance (Guest)
on 24.08.2007 17:04
(Received via mailing list)
I think that the fields order is wrong.
I found this:
TAG song artist album year comment genre

Cédric
Posted by Eugene Kalenkovich (Guest)
on 24.08.2007 17:16
(Received via mailing list)
"Ruby Quiz" <james@grayproductions.net> wrote in message
news:20070824123444.YGDK22245.eastrmmtao104.cox.net@eastrmimpo02.cox.net...
> TAG song album artist comment year genre
>

You've misplaced year and comment.
http://www.id3.org/ID3v1

--EK
Posted by James Gray (bbazzarrakk)
on 24.08.2007 17:17
(Received via mailing list)
On Aug 24, 2007, at 10:03 AM, Cédric Finance wrote:

> I think that the fields order is wrong.
> I found this:
> TAG song artist album year comment genre

You are right.  Sorry about that.  I've fixed it on the Ruby Quiz site.

James Edward Gray II
Posted by John Miller (jfmiller28)
on 24.08.2007 17:29
James Gray wrote:

> The format of the tag is as follows:

I assume that the song album artist and comment fields are NUL padded?

The 4 bytes of Year are 4 character and not a 32bit number?

John Miller
Posted by James Gray (bbazzarrakk)
on 24.08.2007 17:44
(Received via mailing list)
On Aug 24, 2007, at 10:29 AM, John Miller wrote:

> James Gray wrote:
>
>> The format of the tag is as follows:
>
> I assume that the song album artist and comment fields are NUL padded?
>
> The 4 bytes of Year are 4 character and not a 32bit number?

Yes and yes.  :)

James Edward Gray II
Posted by Brad Ediger (Guest)
on 26.08.2007 00:05
(Received via mailing list)
On Aug 24, 2007, at 10:15 AM, James Edward Gray II wrote:

> On Aug 24, 2007, at 10:03 AM, Cédric Finance wrote:
>
>> I think that the fields order is wrong.
>> I found this:
>> TAG song artist album year comment genre
>
> You are right.  Sorry about that.  I've fixed it on the Ruby Quiz  
> site.

You fixed one problem, but artist and album are still flipped.

(this didn't come through the first time, trying without the S/MIME
signature)
Posted by James Gray (bbazzarrakk)
on 26.08.2007 00:16
(Received via mailing list)
On Aug 25, 2007, at 5:04 PM, Brad Ediger wrote:

>
> You fixed one problem, but artist and album are still flipped.

Egad.  I must have had a massive dyslexia attack when I wrote that quiz.

It should be fixed now.

James Edward Gray II
Posted by Jesse Merriman (Guest)
on 26.08.2007 15:46
Attachment: hashy.rb (496 Bytes)
Attachment: genres.rb (3,4 KB)
Attachment: id3_tags.rb (1,3 KB)
(Received via mailing list)
Here's my solution. Should be pretty straightforward.
id3_tags.rb takes a list of filenames as arguments:

$ ./id3_tags.rb 04_Prepare_Yourself.mp3 05_Moonloop.mp3
04_Prepare_Yourself.mp3:
  song: Prepare Yourself
  track: 4
  artist: Porcupine Tree
  comment: some comment
  year: 1995
  album: The Sky Moves Sideways
  genre: Progressive Rock

05_Moonloop.mp3:
  song: Moonloop
  track: 5
  artist: Porcupine Tree
  comment: test comment
  year: 1995
  album: The Sky Moves Sideways
  genre: Progressive Rock
Posted by Ken Bloom (Guest)
on 26.08.2007 16:42
(Received via mailing list)
class NoID3Error < StandardError
end

class ID3
  Genres=" Blues
    Classic Rock
    Country
    Dance
    Disco
    Funk
    Grunge
    Hip-Hop
    Jazz
    Metal
    New Age
    Oldies
    Other
    Pop
    R&B
    Rap
    Reggae
    Rock
    Techno
    Industrial
    Alternative
    Ska
    Death Metal
    Pranks
    Soundtrack
    Euro-Techno
    Ambient
    Trip-Hop
    Vocal
    Jazz+Funk
    Fusion
    Trance
    Classical
    Instrumental
    Acid
    House
    Game
    Sound Clip
    Gospel
    Noise
    AlternRock
    Bass
    Soul
    Punk
    Space
    Meditative
    Instrumental Pop
    Instrumental Rock
    Ethnic
    Gothic
    Darkwave
    Techno-Industrial
    Electronic
    Pop-Folk
    Eurodance
    Dream
    Southern Rock
    Comedy
    Cult
    Gangsta
    Top 40
    Christian Rap
    Pop/Funk
    Jungle
    Native American
    Cabaret
    New Wave
    Psychadelic
    Rave
    Showtunes
    Trailer
    Lo-Fi
    Tribal
    Acid Punk
    Acid Jazz
    Polka
    Retro
    Musical
    Rock & Roll
    Hard Rock
    Folk
    Folk-Rock
    National Folk
    Swing
    Fast Fusion
    Bebob
    Latin
    Revival
    Celtic
    Bluegrass
    Avantgarde
    Gothic Rock
    Progressive Rock
    Psychedelic Rock
    Symphonic Rock
    Slow Rock
    Big Band
    Chorus
    Easy Listening
    Acoustic
    Humour
    Speech
    Chanson
    Opera
    Chamber Music
    Sonata
    Symphony
    Booty Bass
    Primus
    Porn Groove
    Satire
    Slow Jam
    Club
    Tango
    Samba
    Folklore
    Ballad
    Power Ballad
    Rhythmic Soul
    Freestyle
    Duet
    Punk Rock
    Drum Solo
    A capella
    Euro-House
    Dance Hall".split("\n").map{|x| x.gsub(/^\s+/,'')}

  attr_accessor :title, :artist, :album, :year, :comment, :genre, :track
  def genre_name
    Genres[@genre]
  end

  def initialize(filename)
    rawdata=open(filename) do |f|
      f.seek(f.lstat.size-128)
      f.read
    end
    tag,@title,@artist,@album,@year,@comment,@genre=rawdata.unpack 
"A3A30A30A30A4A30c"
    if rawdata[3+30+30+30+4+28]==0
      @track=rawdata[3+30+30+30+4+29]
      @track=nil if @track==0
    end
    if tag!="TAG"
      raise NoID3Error
    end
  end
end
Posted by Jesse Merriman (Guest)
on 26.08.2007 17:03
Attachment: id3_tags.rb (1,3 KB)
(Received via mailing list)
On Sunday 26 August 2007, Jesse Merriman wrote:
> Here's my solution. Should be pretty straightforward.

Here's a very slightly improved version of id3_tags.rb (which still 
requires
the other two files I submitted, unchanged). The only change is less 
ugly
use of String#[], and no more Null constant.
Posted by come (Guest)
on 26.08.2007 18:56
(Received via mailing list)
Hi,

Here is my solution :

require "delegate"

class ID3Tags < DelegateClass(Struct)
  MP3_TYPE=%w(Blues Classic Rock Country Dance Disco Funk Grunge Hip-
Hop Jazz Metal New Age Oldies Other Pop R&B Rap Reggae Rock Techno
Industrial Alternative Ska Death Metal Pranks Soundtrack Euro-Techno
Ambient Trip-Hop Vocal Jazz+Funk Fusion Trance Classical Instrumental
Acid House Game Sound Clip Gospel Noise AlternRock Bass Soul Punk
Space Meditative Instrumental Pop Instrumental Rock Ethnic Gothic
Darkwave Techno-Industrial Electronic Pop-Folk Eurodance Dream
Southern Rock Comedy Cult Gangsta Top 40 Christian Rap Pop/Funk Jungle
Native American Cabaret New Wave Psychadelic Rave Showtunes Trailer Lo-
Fi Tribal Acid Punk Acid Jazz Polka Retro Musical Rock & Roll Hard
Rock Folk Folk-Rock National Folk Swing Fast Fusion Bebob Latin
Revival Celtic Bluegrass Avantgarde Gothic Rock Progressive Rock
Psychedelic Rock Symphonic Rock Slow Rock Big Band Chorus Easy
Listening Acoustic Humour Speech Chanson Opera Chamber Music Sonata
Symphony Booty Bass Primus Porn Groove Satire Slow Jam Club Tango
Samba Folklore Ballad Power Ballad Rhythmic Soul Freestyle Duet Punk
Rock Drum Solo A capella Euro-House Dance Hall)

  Tag=Struct.new(:song,:album,:artist,:year,:comment,:track,:genre)

  def initialize(file)
    raise "No ID3 Tag detected" unless File.size(file) > 128
    File.open(file,"r") do |f|
      f.seek(-128, IO::SEEK_END)
      tag = f.read.unpack('A3A30A30A30A4A30C1')
      raise "No ID3 Tag detected" unless tag[0] == 'TAG'
      if tag[5][-2] == 0 and tag[5][-1] != 0
  tag[5]=tag[5].unpack('A28A1C1').values_at(0,2)
      else
  tag[5]=[tag[5],nil]
      end
      super(@tag=Tag.new(*tag.flatten[1..-1]))
    end
  end

    def to_s
      members.each do |name|
  puts "#{name} : #{send(name)}"
      end
    end

    def genre
      MP3_TYPE[@tag.genre]
    end

end

Come
Posted by Brad Ediger (Guest)
on 26.08.2007 19:34
(Received via mailing list)
One of the biggest problems in software development is feature creep.
In the case of this Quiz, specification creep was the culprit, with
the spec being changed two times in two days. No offense intended,
JEG2 ;-)

Luckily, we can use the mighty power of Ruby to make our application
impervious to such changes, and save a couple heredocs to boot.

-------------------------

#!/usr/bin/env ruby -rubygems

%w(hpricot open-uri).each(&method(:require))

fields, genres = (Hpricot(open("http://www.rubyquiz.com/
quiz136.html")) / "p.example").map{|e| e.inner_html}
fields = fields.split
genres = genres.split "<br />"

values = IO.read(ARGV.first)[-128..-1].unpack("A3 A30 A30 A30 A4 A30 A")

unless values.first == 'TAG'
   puts "No ID3 tag found"
   exit 1
end

fields.zip(values).each do |field, value|
   case field # this feels dirty
   when 'TAG': # nada
   when 'genre': puts "#{field}: #{genres[value[0]]}"
   when 'comment'
     puts "#{field}: #{value}"
     if value[28].to_i.zero? && !value[29].to_i.zero? # ID3v1.1
       puts "track: #{value[29]}"
     end
   else puts "#{field}: #{value}"
   end
end
Posted by Brad Ediger (Guest)
on 26.08.2007 19:40
(Received via mailing list)
On Aug 26, 2007, at 11:55 AM, come wrote:

> Ambient Trip-Hop Vocal Jazz+Funk Fusion Trance Classical Instrumental
> Listening Acoustic Humour Speech Chanson Opera Chamber Music Sonata
> Symphony Booty Bass Primus Porn Groove Satire Slow Jam Club Tango
> Samba Folklore Ballad Power Ballad Rhythmic Soul Freestyle Duet Punk
> Rock Drum Solo A capella Euro-House Dance Hall)

That's not going to work like you think it will:

 >> %w(New Age)
=> ["New", "Age"]
Posted by James Gray (bbazzarrakk)
on 26.08.2007 20:09
(Received via mailing list)
On Aug 26, 2007, at 8:45 AM, Jesse Merriman wrote:

> Here's my solution.

Here's my own:

#!/usr/bin/env ruby -w

GENRES = %w[ Blues Classic\ Rock Country Dance Disco Funk Grunge Hip-
Hop Jazz
              Metal New\ Age Oldies Other Pop R&B Rap Reggae Rock Techno
              Industrial Alternative Ska Death\ Metal Pranks Soundtrack
              Euro-Techno Ambient Trip-Hop Vocal Jazz+Funk Fusion Trance
              Classical Instrumental Acid House Game Sound\ Clip
Gospel Noise
              AlternRock Bass Soul Punk Space Meditative Instrumental
\ Pop
              Instrumental\ Rock Ethnic Gothic Darkwave Techno-
Industrial
              Electronic Pop-Folk Eurodance Dream Southern\ Rock
Comedy Cult
              Gangsta Top\ 40 Christian\ Rap Pop/Funk Jungle Native\
American
              Cabaret New\ Wave Psychadelic Rave Showtunes Trailer Lo-
Fi Tribal
              Acid\ Punk Acid\ Jazz Polka Retro Musical Rock\ &\ Roll
Hard\ Rock
              Folk Folk-Rock National\ Folk Swing Fast\ Fusion Bebob
Latin
              Revival Celtic Bluegrass Avantgarde Gothic\ Rock
Progressive\ Rock
              Psychedelic\ Rock Symphonic\ Rock Slow\ Rock Big\ Band
Chorus
              Easy\ Listening Acoustic Humour Speech Chanson Opera
Chamber\ Music
              Sonata Symphony Booty\ Bass Primus Porn\ Groove Satire
Slow\ Jam
              Club Tango Samba Folklore Ballad Power\ Ballad Rhythmic
\ Soul
              Freestyle Duet Punk\ Rock Drum\ Solo A\ capella Euro-House
              Dance\ Hall ]

abort "Usage: #{File.basename($PROGRAM_NAME)} MP3_FILE" unless
ARGV.size == 1

tag, song, artist, album, year, comment, genre =
   ARGF.read[-128..-1].unpack("A3A30A30A30A4A30C")
if comment.size == 30 and comment[28] == ?\0
   track   = comment[29]
   comment = comment[0..27].strip
else
   track = nil
end

abort "ID3v1 tag not found." unless tag == "TAG"

puts "Song:     #{song}"
puts "Artist:   #{artist}"
puts "Album:    #{album}"
puts "Comment:  #{comment}" unless comment.empty?
puts "Track:    #{track}"   unless track.nil?
puts "Year:     #{year}"
puts "Genre:    #{GENRES[genre] || 'Unknown'}"

__END__

James Edward Gray II
Posted by James Gray (bbazzarrakk)
on 26.08.2007 20:21
(Received via mailing list)
On Aug 26, 2007, at 12:32 PM, Brad Ediger wrote:

> One of the biggest problems in software development is feature  
> creep. In the case of this Quiz, specification creep was the  
> culprit, with the spec being changed two times in two days. No  
> offense intended, JEG2 ;-)

I just did that to inspire you to such a clever solution.  ;)

James Edward Gray II
Posted by come (Guest)
on 26.08.2007 20:51
(Received via mailing list)
Yes, you are right, I answered a little bit to fast ;-)
Posted by Justin Ethier (Guest)
on 26.08.2007 21:03
(Received via mailing list)
Hello,

My solution is straightforward. It seeks backwards from the end of file 
to
read the ID3 tag, and then uses a regular expression to parse the tag. I
then manually extract the track number if found:

genres = ["Blues",
... (snip) ...
"Dance Hall"]

filename = ARGV[0]

if File.exists?(filename)
  f = File.new(filename, "rb")

  # Read ID3 tag from file
  f.seek(-128, IO::SEEK_END)
  data = f.read
  f.close

  # Parse the ID3 tag, order is [TAG song artist album year comment 
genre]
  match_data = 
/(TAG)(.{30})(.{30})(.{30})(.{4})(.{30})(.{1})/.match(data)

  if match_data != nil

    # If 29th byte of comment is 0, parse the field to obtain ID3 v1.1 
track
number
    if match_data[6][28] == 0
      comment = match_data[6].slice(0, 28)
      track_num = match_data[6][29].to_i.to_s
    else
      comment = match_data[6]
      track_num = ""
    end

    puts "   Song: #{match_data[2].strip}"
    puts " Artist: #{match_data[3].strip}"
    puts "  Album: #{match_data[4].strip}"
    puts "   Year: #{match_data[5]}"
    puts "Comment: #{comment.strip}"
    puts "  Track: #{track_num}"
    puts "  Genre: #{genres[match_data[7].to_i]}"
  end
end

Here is a pastie of the complete program - http://pastie.caboo.se/91121
Thanks,

Justin
Posted by come (Guest)
on 26.08.2007 21:10
(Received via mailing list)
My corrected version :


require "delegate"

class ID3Tags < DelegateClass(Struct)
  MP3_TYPE=["Blues","Classic
Rock","Country","Dance","Disco","Funk","Grunge","Hip-
Hop","Jazz","Metal","New
Age","Oldies","Other","Pop","R&B","Rap","Reggae","Rock","Techno","Industrial","Alternative","Ska","Death
Metal","Pranks","Soundtrack","Euro-Techno","Ambient","Trip-
Hop","Vocal","Jazz
+Funk","Fusion","Trance","Classical","Instrumental","Acid","House","Game","Sound
Clip","Gospel","Noise","AlternRock","Bass","Soul","Punk","Space","Meditative","Instrumental
Pop","Instrumental Rock","Ethnic","Gothic","Darkwave","Techno-
Industrial","Electronic","Pop-Folk","Eurodance","Dream","Southern
Rock","Comedy","Cult","Gangsta","Top 40","Christian Rap","Pop/
Funk","Jungle","Native American","Cabaret","New
Wave","Psychadelic","Rave","Showtunes","Trailer","Lo-
Fi","Tribal","Acid Punk","Acid Jazz","Polka","Retro","Musical","Rock &
Roll","Hard Rock","Folk","Folk-Rock","National Folk","Swing","Fast
Fusion","Bebob","Latin","Revival","Celtic","Bluegrass","Avantgarde","Gothic
Rock","Progressive Rock","Psychedelic Rock","Symphonic Rock","Slow
Rock","Big Band","Chorus","Easy
Listening","Acoustic","Humour","Speech","Chanson","Opera","Chamber
Music","Sonata","Symphony","Booty Bass","Primus","Porn
Groove","Satire","Slow
Jam","Club","Tango","Samba","Folklore","Ballad","Power
Ballad","Rhythmic Soul","Freestyle","Duet","Punk Rock","Drum Solo","A
capella","Euro-House","Dance Hall"]

  Tag=Struct.new(:song,:album,:artist,:year,:comment,:track,:genre)

  def initialize(file)
    raise "No ID3 Tag detected" unless File.size(file) > 128
    File.open(file,"r") do |f|
      f.seek(-128, IO::SEEK_END)
      tag = f.read.unpack('A3A30A30A30A4A30C1')
      raise "No ID3 Tag detected" unless tag[0] == 'TAG'
      if tag[5][-2] == 0 and tag[5][-1] != 0
  tag[5]=tag[5].unpack('A28A1C1').values_at(0,2)
      else
  tag[5]=[tag[5],nil]
      end
      super(@tag=Tag.new(*tag.flatten[1..-1]))
    end
  end

    def to_s
      members.each do |name|
  puts "#{name} : #{send(name)}"
      end
    end

    def genre
      MP3_TYPE[@tag.genre]
    end

end
Posted by Brad Ediger (Guest)
on 26.08.2007 21:15
(Received via mailing list)
On Aug 26, 2007, at 1:08 PM, James Edward Gray II wrote:

>   ARGF.read[-128..-1].unpack("A3A30A30A30A4A30C")

Well played, sir. I always forget about ARGF. And to think I call
myself a Perl nerd.

-be
Posted by Ken Bloom (Guest)
on 26.08.2007 21:40
(Received via mailing list)
On Mon, 27 Aug 2007 02:32:05 +0900, Brad Ediger wrote:

> 
> %w(hpricot open-uri).each(&method(:require))
> 
> fields, genres = (Hpricot(open("http://www.rubyquiz.com/quiz136.html")) / "p.example").map{|e| e.inner_html}
> fields = fields.split
> genres = genres.split "<br />"

You hard-coded the value of the unpack field. If you wanted to download 
the spec
properly, you'd generate that from the spec like follows. (Picking up 
from the end
of what I've quoted above)

unpacktypes=Hash.new("A30")
unpacktypes["TAG"]="A3"
unpacktypes["year"]="A4"
unpacktypes["genre"]="c"
unpackstr=fields.map{|x| unpacktypes[x]}.join

id3=Hash.new
raw=open('/home/bloom/scratch/music/rondo.mp3') do |f|
  f.seek(f.lstat.size-128)
  f.read
end

values=raw.unpack(unpackstr)

fields.zip(values).each do |field,value|
  id3[field]=value
end

fail if id3["TAG"]!="TAG"

if id3["comment"].length==30 and id3["comment"][-2]==0
  id3["track"]=id3["comment"][-1]
  id3["comment"]=id3["comment"][0..-2].strip
end

id3["genre"]=genres[id3["genre"]] || "Unknown"
p id3
Posted by Ken Bloom (Guest)
on 26.08.2007 21:42
(Received via mailing list)
On Sun, 26 Aug 2007 09:16:32 -0500, Ken Bloom wrote:

>     Grunge
>     Rock
>     Vocal
>     Noise
>     Darkwave
>     Christian Rap
>     Tribal
>     Swing
>     Symphonic Rock
>     Sonata
>     Ballad
>   attr_accessor :title, :artist, :album, :year, :comment, :genre, :track
>     "A3A30A30A30A4A30c" if rawdata[3+30+30+30+4+28]==0
>       @track=rawdata[3+30+30+30+4+29]
>       @track=nil if @track==0
>     end
>     if tag!="TAG"
>       raise NoID3Error
>     end
>   end
> end

Apparently unpack('A30') doesn't work quite the way I thought --
it only shortens the string if the string ends in null characters.
If there are nulls in the middle, then those and the characters after
them are preserved.

--Ken
Posted by Brad Ediger (Guest)
on 26.08.2007 21:50
(Received via mailing list)
On Aug 26, 2007, at 2:40 PM, Ken Bloom wrote:

>>
>
> You hard-coded the value of the unpack field.

I know, I felt bad about doing it (and this was more of a "ha-ha,
have fun with the Quiz" submission than a "use this in production"
submission).

I was about to rewrite it to scrape the actual data structure from
the table in http://www.id3.org/ID3v1, but then I'd have to find
another quasi-official source for the genre list, and it began to
feel more like work.

I like your solution. Yes, I should have used a "c" for the genre
field, but my brain wasn't working.

-be
Posted by Johannes Held (Guest)
on 26.08.2007 22:56
(Received via mailing list)
Brad Ediger schrieb:
> On Aug 26, 2007, at 1:08 PM, James Edward Gray II wrote:
> 
>>   ARGF.read[-128..-1].unpack("A3A30A30A30A4A30C")
> 
> Well played, sir. I always forget about ARGF. And to think I call myself 
> a Perl nerd.
What the heck is ARGF?
Posted by Joel VanderWerf (Guest)
on 26.08.2007 23:06
(Received via mailing list)
Johannes Held wrote:
> What the heck is ARGF?

It's a pseudo-IO that reads the concatenation of the files named in
ARGV, unless ARGV is empty, in which case it just reads standard input.
It's very useful in writing little command-line programs that can be
used as filters or on a list of named files (after you delete any
switches or options from the command line).

[~] cat >foo.txt
foo
[~] cat >bar.txt
bar
[~] ruby -e 'puts ARGF.read' foo.txt bar.txt
foo
bar

[~] echo zap | ruby -e 'puts ARGF.read'
zap
Posted by Tom Metge (Guest)
on 26.08.2007 23:17
Attachment: id3_tag_genre.rb (1,1 KB)
(Received via mailing list)
Hey all, here's another one for you.  I admit that there isn't anything
special about it... I think it's one of the more direct solutions (i.e.
Nothing clever here guys).  I didn't see a reason to include the entire
genre, so it's attached in a separate file.  It simply declares a 
constant
(an array which is indexed in read_tags).

Tom


--BEGIN SOLUTION--
require 'id3_tag_genre'

class NoTagError < RuntimeError; end

class Mp3
  attr_reader :song, :artist, :album, :year, :comment, :genre, :track

  def initialize(file)
    read_tags(file)
  end

  def read_tags(file)
    begin
      size = File.stat(file).size
      f = File.open(file)
      f.pos = size - 128
      tag = f.read
      raise NoTagError unless tag[0..2] == "TAG"
      @song = tag[3..32].strip
      @artist = tag[33..62].strip
      @album = tag[63..92].strip
      @year = tag[93..96].strip
      @comment = tag[97..126]
        if @comment[28] == 0 && @comment[29] != 0
          @track = @comment[29..29].to_i
          @comment = @comment[0..28].strip
        end
      @genre = Genre[tag[127]]
    rescue NoTagError
      puts "No tags found!"
      return false
    end
    true
  end
end
Posted by Johannes Held (Guest)
on 27.08.2007 00:41
(Received via mailing list)
Joel VanderWerf schrieb:
> Johannes Held wrote:
>> What the heck is ARGF?> 
> It's a pseudo-IO that reads the concatenation of the files named in 
> ARGV, unless ARGV is empty, in which case it just reads standard input. 
> It's very useful in writing little command-line programs that can be 
> used as filters or on a list of named files (after you delete any 
> switches or options from the command line).
Thank you.
Posted by Erik Bryn (ebryn)
on 27.08.2007 01:20
(Received via mailing list)
Here's mine. Takes a directory as input and exports a tab-seperated
list.

- Erik

--

GENRES = ["Blues", "Classic Rock", "Country", "Dance", "Disco",
"Funk", "Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies",
"Other", "Pop", "R&B", "Rap", "Reggae", "Rock", "Techno",
"Industrial", "Alternative", "Ska", "Death Metal", "Pranks",
"Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz
+Funk", "Fusion", "Trance", "Classical", "Instrumental", "Acid",
"House", "Game", "Sound Clip", "Gospel", "Noise", "AlternRock",
"Bass", "Soul", "Punk", "Space", "Meditative", "Instrumental Pop",
"Instrumental Rock", "Ethnic", "Gothic", "Darkwave", "Techno-
Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", "Southern
Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", "Pop/
Funk", "Jungle", "Native American", "Cabaret", "New Wave",
"Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal",
"Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll",
"Hard Rock", "Folk", "Folk-Rock", "National Folk", "Swing", "Fast
Fusion", "Bebob", "Latin", "Revival", "Celtic", "Bluegrass",
"Avantgarde", "Gothic Rock", "Progressive Rock", "Psychedelic Rock",
"Symphonic Rock", "Slow Rock", "Big Band", "Chorus", "Easy Listening",
"Acoustic", "Humour", "Speech", "Chanson", "Opera", "Chamber Music",
"Sonata", "Symphony", "Booty Bass", "Primus", "Porn Groove", "Satire",
"Slow Jam", "Club", "Tango", "Samba", "Folklore", "Ballad", "Power
Ballad", "Rhythmic Soul", "Freestyle", "Duet", "Punk Rock", "Drum
Solo", "A capella", "Euro-House", "Dance Hall"]
FIELDS = [:song, :artist, :album, :year, :comment, :genre]

def find_track_number(fields)
  if fields[:comment][-2] == 0 && fields[:comment][-1] != 0
    fields[:track_number] = fields[:comment].slice!(-2..-1)[1]
    fields[:comment].strip!
  end
end

abort "Usage: #{File.basename($PROGRAM_NAME)} <dir>" unless ARGV.size
== 1
Dir["#{ARGV.first}/*.mp3"].each do |path|
  File.open(path, 'rb') do |f|
    f.seek(-128, IO::SEEK_END)
    bytes = f.read
    next if bytes.slice!(0..2) != "TAG"

    tags = Hash[*FIELDS.zip(bytes.unpack('A30A30A30A4A30C')).flatten]
    tags[:genre] = GENRES[tags[:genre]]
    find_track_number(tags)
    puts "#{File.basename(path)}\t#{tags[:artist]}\t#{tags[:song]}
\t#{tags[:album]}\t#{tags[:track_number]}\t#{tags[:year]}
\t#{tags[:genre]}\t#{tags[:comment]}"
  end
end
Posted by Juan Matias (jmrepetti)
on 27.08.2007 03:55
(Received via mailing list)
class ID3reader

  attr_reader :song, :album, :artist, :comment,:year,:genre,:track
  TAG = 3
  SONG = 30
    ALBUM = 30
    ARTIST = 30
  YEAR = 4
    COMMENT = 30
    GENRE = 1
  GENRE_LIST = ["Blues","Classic
Rock","Country","Dance","Disco","Funk","Grunge","Hip-Hop","Jazz","Metal",
                "New
Age","Oldies","Other","Pop","R&B","Rap","Reggae","Rock","Techno","Industrial",
                "Alternative","Ska","Death
Metal","Pranks","Soundtrack","Euro-Techno","Ambient","Trip-Hop","Vocal",

"Jazz+Funk","Fusion","Trance","Classical","Instrumental","Acid","House","Game","Sound
Clip",

"Gospel","Noise","AlternRock","Bass","Soul","Punk","Space","Meditative","Instrumental
Pop","Instrumental Rock",

"Ethnic","Gothic","Darkwave","Techno-Industrial","Electronic","Pop-Folk","Eurodance","Dream","Southern
Rock",
                "Comedy","Cult","Gangsta","Top 40","Christian
Rap","Pop/Funk","Jungle","Native American","Cabaret","New Wave",

"Psychadelic","Rave","Showtunes","Trailer","Lo-Fi","Tribal","Acid
Punk","Acid Jazz","Polka","Retro","Musical",
                "Rock & Roll","Hard Rock","Folk","Folk-Rock","National
Folk","Swing","Fast Fusion","Bebob","Latin","Revival",
                "Celtic","Bluegrass","Avantgarde","Gothic 
Rock","Progressive
Rock","Psychedelic Rock","Symphonic Rock","Slow Rock",
                "Big Band","Chorus","Easy
Listening","Acoustic","Humour","Speech","Chanson","Opera",
                "Chamber Music","Sonata","Symphony","Booty
Bass","Primus","Porn Groove","Satire",
                "Slow
Jam","Club","Tango","Samba","Folklore","Ballad","Power Ballad","Rhythmic
Soul",
                "Freestyle","Duet","Punk Rock","Drum Solo","A
capella","Euro-House","Dance Hall"]


  def initialize(mp3_file_path)

    mp3file = File.open(mp3_file_path,"r")
    mp3file.seek(-128, IO::SEEK_END)

    unless mp3file.read(TAG) != "TAG"
      @song = mp3file.read(SONG).strip
        @artist = mp3file.read(ARTIST).strip
      @album = mp3file.read(ALBUM).strip
      @year = mp3file.read(YEAR).strip
        @comment = mp3file.read(COMMENT)
      unless (@comment[28..29] =~ /\0[:cntrl:]?/).nil?
        @track = @comment[29].to_i
        @comment[29]=0
      end
      @comment.strip!
      @genre = GENRE_LIST[mp3file.read(GENRE).to_i]
      mp3file.close
    end
  end
end

irb(main):001:0> require 'id3reader'

irb(main):003:0> ID3reader.new("5.mp3") #No tag's
=> #<ID3reader:0xb7ce3814>

irb(main):004:0> ID3reader.new("10.mp3")
=> #<ID3reader:0xb7cde1c0 @year="1995", @genre="Blues", 
@album="undiscovered
soul", @track=10, @artist="richie sambora", @comment="Otro
temazo!oooooooooooooooo", @song="who I am">
irb(main):005:0>

Juan Matias Repetti
Posted by Alpha Chen (Guest)
on 27.08.2007 18:05
(Received via mailing list)
My fairly straightforward solution:

class ID3
  genre_list = <<-GENRES
Blues
... # snipped for brevity
Dance Hall
  GENRES

  GENRE_LIST = genre_list.split("\n")
  TAGS = [ :title, :artist, :album, :year, :comment, :track, :genre ]

  attr_accessor *TAGS

  def initialize(filename)
    id3 = File.open(filename) do |mp3|
      mp3.seek(-128, IO::SEEK_END)
      mp3.read
    end

    raise "No ID3 tags" if id3 !~ /^TAG/

    @title, @artist, @album, @year, @comment, @genre =
id3.unpack('xxxA30A30A30A4A30C1')
    @comment, @track = @comment.unpack('Z*@28C1') if @comment =~ /
\0.$/

    @genre = GENRE_LIST[@genre]
  end
end

if __FILE__ == $0
  id3 = ID3.new(ARGV.shift)
  ID3::TAGS.each do |tag|
    puts "#{tag.to_s.capitalize.rjust(8)}: #{id3.send(tag)}"
  end
end
Posted by John Miller (jfmiller28)
on 27.08.2007 18:12
Here is my go at things:

__BEGIN__
#Note: this script assumes Ruby 1.8.6 style handeling of strings.  Some 
changes
#will need to be made for Ruby 1.9 to work correctly

require 'genre.rb' #an array of the official genera list

def id3(filename)
  id3 =  File.open(filename,'r') do |file|
    file.seek(-128,IO::SEEK_END) #get to the end of the file
    file.read(128)
  end
  return "" unless id3 #protect against read error
  if id3.slice(0,3) == "TAG"
     #Skip the first 3 bytes grab three thirty byte fields
     #and a 4 byte field dropping trailing whitespace.
     #While we can assume the old style comment field and
     #take 30 bytes (we'll com back for the track number later)
     #we must use 'Z' instead of 'A' to avoid having the track
     #show up in our comment field.
     #The last byte is the genre index.
    song,artist,album,year,comment,genre = id3.unpack 
"x3A30A30A30A4Z30C"
     #grab the track with a pain slice
    track = id3.slice(-2) if id3.slice(-3) == 0 && id3.slice(-2) != 0
    desc = "#{artist}: #{album}(#{year})\n"
    desc << "  #{song}.  "
    desc << "tr. #{track}" if track
    desc <<"\n"
    desc << "  Comment:  #{comment.chomp(" ")}\n" if comment.length != 0
    desc << "    Genre:  #{Genres[genre]}\n"
    return desc
  end

  return "" #tag not forund

end

#usage id3.rb filename [filename*]
ARGV.each do |filename|
  puts filename
  puts id3(filename) if File.exists? filename
  puts "\n"
end

__END__

I think the only real difference between what I'm seeing on this list 
and my own solution is the unpack string.  The 'Comment' filed must use 
'Z' and strip trailing white space separately otherwise the track number 
could get pulled and stuck on the end of the output.

I like the use of ARGF in other implementations.  Something new to put 
in my hat.

John Miller
Posted by Matthew Moss (Guest)
on 30.08.2007 01:57
(Received via mailing list)
I've been extremely busy lately, but I wanted to give this one a try.
This solution is not complete as far as the problem specification
goes, but my bit o' metaprogramming-type stuff works, though I'd have
liked to push it further.

class ID3

   @@recLen = 0

   def ID3.field(name, len, flags=[])
      class_eval(%Q[
         def #{name}
            @data[#{@@recLen}, #{len}].strip
         end
      ])

      unless flags.include?(:readonly)
         class_eval(%Q[
            def #{name}=(val)
               # need to pad val to len
               @data[#{@@recLen}, #{len}] = val.ljust(#{len}, "\000")
            end
         ])
      end
      @@recLen += len
   end

   # --------------------------------------------------------------
   #     name, length,       flags
   field :sig,      3,  [:readonly]
   field :song,    30
   field :album,   30
   field :artist,  30
   field :year,     4
   field :comment, 30
   field :genre,    1

   TAG_SIG  = "TAG"
   TAG_SIZE = @@recLen
   raise "ID3 tag size not 128!" unless TAG_SIZE == 128

   # --------------------------------------------------------------

   def ID3.createFromBuffer(buffer)
      ID3.new(buffer)
   end

   def ID3.createFromFile(fname)
      size = File.size?(fname)
      raise "Missing or empty file" unless size
      raise "Invalid file" if size < TAG_SIZE

      # Read the tag and pass to createFromBuffer
      open(fname, "rb") do |f|
         f.seek(-TAG_SIZE, IO::SEEK_END)
         createFromBuffer(f.read(TAG_SIZE))
      end
   end

   # --------------------------------------------------------------

   def initialize(data)
      @data = data

      raise "Wrong buffer size" unless @data.size == TAG_SIZE
      raise "ID3 tag not found" unless self.sig == TAG_SIG
   end

end


id = ID3.createFromFile("maple-leaf-rag.mp3")
puts id.song
Posted by James Gray (bbazzarrakk)
on 30.08.2007 04:31
(Received via mailing list)
On Aug 29, 2007, at 6:56 PM, Matthew Moss wrote:

> I've been extremely busy lately, but I wanted to give this one a try.
> This solution is not complete as far as the problem specification
> goes, but my bit o' metaprogramming-type stuff works, though I'd have
> liked to push it further.

This is a very clever solution.  I have one suggestion though…

> class ID3
>
>    @@recLen = 0
>
>    def ID3.field(name, len, flags=[])

Changing flags=[] to *flags gives a nicer interface, I think.

James Edward Gray II
Posted by Matthew Moss (Guest)
on 30.08.2007 17:31
(Received via mailing list)
On 8/29/07, James Edward Gray II <james@grayproductions.net> wrote:
> >
> >    @@recLen = 0
> >
> >    def ID3.field(name, len, flags=[])
>
> Changing flags=[] to *flags gives a nicer interface, I think.

True... I had thought of that this morning, though I also wanted to
add a conversion parameter... so a lambda or block could be provided
that would convert between the record's string data and an integer
(e.g. the ID3 year).
Posted by Matthew Moss (Guest)
on 30.08.2007 17:36
(Received via mailing list)
On 8/30/07, Matthew Moss <matthew.moss.coder@gmail.com> wrote:
> > > class ID3
> (e.g. the ID3 year).
And, of course, the whole field/record thingy should be separated out
into its own class/module/whatever.  I did see bit-struct out there,
and considered a solution using that, but it felt weird to be doing
things at a bit-level, so I just kept on with my own.