# $Id$
# = Description
#
# ruby-ogginfo gives you access to low level information on ogg files
# (bitrate, length, samplerate, encoder, etc... ), as well as tag.
# It is written in pure ruby.
#
#
# = Download
#
#
# http://rubyforge.org/projects/ruby-ogginfo/
#
#
# = Generate documentation
# 
# rdoc --template=kilmer ogginfo.rb
#
#
# = Changelog
#
# [0.1 20/06/2004]
# 
# * first public version
#
#
# License:: Ruby
# Author:: Guillaume Pierronnet (mailto:moumar_AT__rubyforge_DOT_org)
# Website:: http://ruby-ogginfo.rubyforge.org/
#
# see http://www.xiph.org/ogg/vorbis/docs.html for documentation on vorbis format
# http://www.xiph.org/ogg/vorbis/doc/v-comment.html

require "iconv"

# Raised on any kind of error related to ruby-ogginfo
class OggInfoError < StandardError ; end

class OggInfo
=begin
  FIELDS_MAPPING = {
    "title" => "songname",
    "artist" => "artist",
    "album" => "album",
    "date" => "year",
    "description" => "comment",
    "tracknumber" => "tracknum",
    "genre" => "genre"
  }
=end
  attr_reader :channels, :samplerate, :bitrate, :tag, :length

  def initialize(filename, charset = "iso-8859-1")
    begin
      @file = File.new(filename, "rb")
      find_page()
      extract_infos()
      find_page()
      extract_tag(charset)
      extract_end()
    ensure
      close
    end
  end

  # "block version" of ::new()
  def self.open(filename)
    m = self.new(filename)
    ret = nil
    if block_given?
      begin
        ret = yield(m)
      ensure
        m.close
      end
    else
      ret = m
    end
    ret
  end

  def close
      @file.close if @file and not @file.closed?
  end

  def hastag?
    not @tag.empty?
  end
  
  def to_s
    "channels #{@channels} samplerate #{@samplerate} bitrate #{@bitrate} length #{@length}"
  end

private
  def find_page
    header = 'OggS' # 0xf4 . 0x67 . 0x 67 . 0x53
    bytes = @file.read(4)
    bytes_read = 4

    while header != bytes
      #raise OggInfoError if bytes_read > 4096 or @file.eof? #bytes.nil?
      raise OggInfoError if @file.eof? #bytes.nil?
      bytes.slice!(0)
      char = @file.read(1)
      bytes_read += 1
      bytes << char
    end
  end

  def extract_infos
#    @bitrate = {}
    @file.seek(35, IO::SEEK_CUR) # seek after "vorbis"
#    @channels, @samplerate, @bitrate["upper"], @bitrate["nominal"], @bitrate["lower"] = 
    @channels, @samplerate, up_br, br, down_br = 
      @file.read(17).unpack("CV4")
    @bitrate = 
      if br == 0
        if up == 2**32 - 1 or down == 2**32 - 1 
	  0
	else
	  (up_br + down_br)/2
	end
      else 
        br
      end
  end

  def extract_tag(charset)
    @tag = {}
    @file.seek(22, IO::SEEK_CUR)
    segs = @file.read(1).unpack("C")[0]
    @file.seek(segs + 7, IO::SEEK_CUR)
    size = @file.read(4).unpack("V")[0]
    @file.seek(size, IO::SEEK_CUR)
    tag_size = @file.read(4).unpack("V")[0]
    ic = Iconv.new(charset, "utf8")
    tag_size.times do |i|
      size = @file.read(4).unpack("V")[0]
      com = @file.read(size)
      begin
        comment = ic.iconv( com )
      rescue Iconv::IllegalSequence, Iconv::InvalidCharacter
        comment = com
      end
      key, val = comment.split(/=/)
      @tag[key.downcase] = val
    end
    ic.close
  end

  def extract_end
    begin #Errno::EINVAL
      @file.seek(-5000, IO::SEEK_END) #FIXME size of seeking
      find_page
      pos = @file.read(6).unpack("x2 V")[0] #FIXME pos is int64
      @length = pos.to_f / @samplerate
    rescue Errno::EINVAL
      @length = 0
    end
  end
end

if $0 == __FILE__
  while filename = ARGV.shift
    puts "Getting info from #{filename}"
    begin
      ogg = OggInfo.new(filename)
    rescue OggInfoError
     puts "error: doesn't appear to be an ogg file"
    else
      puts ogg
      ogg.tag.each { |key, value|
        puts "#{key} => #{value}"
      }
    end
    puts
  end
end
