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
-- vjoel : Joel VanderWerf : path berkeley edu : 510 665 3407
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
> 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).
__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.
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.
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
> 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.
> 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.
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).
> On 8/29/07, James Edward Gray II <ja...@grayproductions.net> wrote: > > 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.
> 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).
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.
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 Kalenkovich'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:
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,tra...@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...