# Cddb module for Ruby. # by jared jennings. liver@soon.com # under Ruby's license http://www.ruby-lang.org/en/LICENSE.txt # i don't really like this design. if you have better ideas # please email me at liver@soon.com # undocumented as of yet. # look at the source. and wavcddb. :) require 'audiofile' require 'socket' require 'timeout' # some algorithms adapted from mp3cddb ... # notice follows # # mp3cddb # ------- # originally: mp3tocddb.pl by Meng Weng Wong (MPEG::MP3Info) # modified by: # module Cddb class Toc def initialize @filenames = [] @titles = [] @artists = [] @discTitle = "" @discArtist = "" @category = "" @discID = "" end def prepare filenames @filenames = filenames @lengths = [] filenames.each do |fn| fil = AudioFile.new fn @lengths.push (fil.frame_count*1000/fil.rate) fil.close end @nrTracks = @lengths.length offset = 150 # first track offset, nearly always. @frameOffsets = [] @totalLength = 0 n = 0 @lengths.each do |length| @frameOffsets.push offset offset += (length * 75 / 1000) n += digitSum(length / 1000) @totalLength += length / 1000 end @discID = sprintf("%08x", ((n % 0xff)<<24) | \ (@totalLength << 8) | \ @nrTracks) end def digitSum num result = 0 while num > 0 result += num % 10 num /= 10 end return result end def setFromCddbInfo lines maxTrackNo = 0 @titles = [] @artists = [] lines.each do |line| if line =~ "TTITLE(.{1,2})=(.*)" trackNo = $1.to_i maxTrackNo = trackNo if trackNo > maxTrackNo @titles[trackNo] = $2 else if line =~ "DTITLE=(.*) / (.*)" @discArtist = $1 @discTitle = $2 else if line =~ "#cddb read (.*) (.*)" @category = $1 @discID = $2 end end end end # split up track titles into artist + title as best we can @titles.each_index do |ind| if (@titles[ind] =~ /(.*?)\s*\/\s*(.*)/) @artists[ind] = $1 @titles[ind] = $2 else if (@titles[ind] =~ /(.*?)\s+-+\s+(.*)/) @titles[ind] = $1 @artists[ind] = $2 else @artists[ind] = nil end end end nrTracks = maxTrackNo + 1 if nrTracks != @nrTracks raise "Info from server has #{nrTracks} track titles, but we want #{@nrTracks}" end if !defined? @discArtist raise "No artist found for CD" end if !defined? @discTitle raise "No title found for CD" end end def inspect # puts out ruby code, so we can have the # beautiful short uninspect shown below string = "" string += "@discArtist=#{@discArtist.inspect}\n" string += "@discTitle=#{@discTitle.inspect}\n" string += "@artists = []\n" string += "@titles = []\n" string += "# (@artists[foo]=nil means use @discArtist)\n" string += "\n" @titles.each_index do |ind| indString = sprintf("%2d",ind) string += " @artists[#{indString}]=#{@artists[ind].inspect}\n" string += " @titles[#{indString}]=#{@titles[ind].inspect }\n" string += "\n" end string += "@discID=#{@discID.inspect}\n" string += "@category=#{@category.inspect}\n" string += "@totalLength=#{@totalLength}\n" string += "@nrTracks=#{@nrTracks}\n" string += "@lengths=#{@lengths.inspect}\n" string += "@frameOffsets=#{@frameOffsets.inspect}\n" string += "@filenames=#{@filenames.inspect}\n" return string end def uninspect inspectString # probably insecure. do not use where security is a must. # (i don't know about security in ruby yet.) self.instance_eval(inspectString) end def lengths @lengths end def totalLength @totalLength end def frameOffsets @frameOffsets end def nrTracks @nrTracks end def discID @discID end def category @category end def filenames @filenames end def artists @artists end def titles @titles end def discArtist @discArtist end def discTitle @discTitle end def artists= newArtists @artists = newArtists end def titles= newTitles @titles = newTitles end def discArtist= newDiscArtist @discArtist = newDiscArtist end def discTitle= newDiscTitle @discTitle = newDiscTitle end def discID= newDiscID @discID = newDiscID end def category= newCat @category = newCat end end class Connection SOFTWARE_NAME = 'ruby-cddb' SOFTWARE_VERSION = '0.1' def initialize server,port @server = server @port = port @login = ENV['USER'] @hostname = `hostname`.chomp print "Connecting to server #{server}:#{port}... " $stdout.flush @sock = TCPSocket.open server, port print " connected.\n" def @sock.getsWithTimeout secs result = "" begin # $stdout.print "<<< " # $stdout.flush timeout(secs) { result = self.gets # $stdout.print result.chomp, "\n" } rescue TimeoutError raise TimeoutError, "recv timed out after #{secs} seconds" end return result end def @sock.printWithTimeout secs, *stuff result = "" begin # $stdout.print ">>> \n" # $stdout.flush timeout(secs) { result = self.print(stuff) # $stdout.print stuff,"\n" } rescue TimeoutError raise TimeoutError, "send timed out after #{secs} seconds" end end banner = @sock.getsWithTimeout 30 case banner[0..2].to_i when 432, 433, 434 @sock.close raise "Connection refused from #{@server}: #{banner}" when 200 print "Handshake with #{@server}:#{@port}.\n" when 201 print "Handshake with #{@server}:#{@port} - read only.\n" @readonly = true else @sock.close raise "Unknown response from server: #{banner}" end @sock.printWithTimeout 15,"cddb hello #{@login} #{@hostname}"\ " #{SOFTWARE_NAME} #{SOFTWARE_VERSION}\n" response = @sock.getsWithTimeout 90 case response[0..2].to_i when 200 ; #OK when 431 @sock.close raise "Server doesn't want to shake hands; connection closed" when 402 print "Server is crazy; thinks we've already shaken hands. Going ahead.\n" when 500 print "Program error; contact ruby-cddb maintainer.\n" else @sock.close raise "Unknown response from server: #{response}" end end def connected? @sock and !@sock.closed? end def query toc if not self.connected? raise "Query attempted while not connected to a server" end query = "cddb query #{toc.discID} #{toc.nrTracks} " toc.frameOffsets.each do |ofs| query += "#{ofs} " end query += "#{toc.totalLength}\n" print "> ",query.chomp, "\n" @sock.printWithTimeout 15, query response = @sock.getsWithTimeout 30 print "< ",response.chomp("\r\n"), "\n" alldiscs = [ ] case response[0..2].to_i when 200 # Exact match found. discinfo = response.split(" ") discinfo.shift # get rid of code @ beginning alldiscs = [ discinfo ] when 211 # Inexact matches found; list follows until terminating '.' until response == "." response = @sock.getsWithTimeout(15) response.chomp!("\r\n") thisinfo = response.split(" ") alldiscs.push thisinfo end alldiscs.pop # get rid of terminating '.' when 202 print "no match found.\n" when 403 print "match found, but corrupt.\n" when 409 print "no handshake! Server doesn't think we've connected properly\n" raise "Unknown connection state" end return alldiscs end def read category,discID if not self.connected? raise "Read attempted while not connected to a server" end query = "cddb read #{category} #{discID}\n" print "> ",query.chomp,"\n" lines = [ "#"+query ] @sock.printWithTimeout 15, query response = @sock.getsWithTimeout 60 case response[0..2].to_i when 210 $stdout.print "Reading #{category} #{discID} " $stdout.flush # OK. Entry follows until terminating '.' line = "" until line == "." line = (@sock.getsWithTimeout 30).chomp "\r\n" lines.push line unless line =~ "^#" $stdout.print "." $stdout.flush end lines.pop # get rid of terminating '.' $stdout.print " done.\n" else raise "Unknown response #{response} from server" end return lines end def close if self.connected? @sock.close end end end end