# $Id: freedb.rb,v 1.22 2003/02/13 15:52:04 moumar Exp $
# = Description
#
# ruby-freedb is a Ruby library who provide access to cddb/freedb servers as
# well as local database, can dump the "discid" from a CD and submit new
# entries to the freedb database.
#
#
# = Download
#
# get tar.gz and debian packages at
# http://davedd.free.fr/ruby-freedb/download/
#
#
# = Installation
#
# CAUTION: Some files have changed since 0.4, please clean up your old ruby-freedb
# (0.3.1 and older) installation before installing this one by deleting our
# freedb_misc.rb and freedb.so.
#
# $ ruby extconf.rb
# $ make
# $ make install
#
#
# = Examples
# see examples/ directory for more advanced examples
#
# === get all possible matches for CD in "/dev/cdrom"
#
# freedb = Freedb.new("/dev/cdrom")
# freedb.fetch
# freedb.results.each { |r| puts r }
#
# === getting full description
# # get "rock" match for this cd
# freedb.get_result("rock")
#
# === make something with your freedb object
# puts freedb.title # disc's title
# puts freedb.artist # disc's artist
# puts freedb.length # disc's length in seconds
# puts freedb.tracks.size # number of tracks on the CD
# puts freedb.tracks[3]["title"] # title of the track 4
# # (indexing begin at 0)
# puts freedb.tracks[5]["length"] # length of track 6 in seconds
#
#
# = Testing
#
# In order to run all tests, you have to burn the "freedb CD Test" at
# http://www.freedb.org/software/freedb_testcd.zip
# and you must be connected to internet.
#
# Test::Unit library is used for tests. see http://testunit.talbott.ws/
#
# $ cd test/
# $ ruby test_all.rb
#
# = ToDo
# * CD-ROM access under Win32
#
# = Changelog
#
# [0.5 07/02/2003]
#
# * submission (http or mail) added
# * fetching from disk in Unix or Windows format added
# * "raw_response" attribute added (raw response from the server) [Fernando Arbeiza ]
# * "tracks" removed (however it can be redefined with 'tracks.collect { |h| h["title"] }'
# * "tracks_ext" renamed to "tracks"
# * "genre" renamed to "category"
# * "exact_genre" renamed to "genre"
# * "get_result(index)": index can be a String that represents the freedb category
# * FetchCGI: does not rely on cgi.rb anymore
# * documentation written with "rdoc"
#
#
# [0.4.2 10/01/2003]
#
# * Fixed a bug in track length computation [Fernando Arbeiza ]
#
#
# [0.4.1 13/10/2002]
#
# * Improved cddb parser [Akinori MUSHA ]
# * Many bugs fixed in freedb_cdrom.c [Akinori MUSHA ]
#
#
# [0.4 28/09/2002]
#
# * length attribute added
# * tracks_ext attribute added
# * fixed a bug in discid computation [Akinori MUSHA ]
# * protocol level handling
# * test suite
# * code refactoring
# * file renaming (change nothing for end users)
#
#
# [0.3.1 30/08/2002]
#
# * genre read-only attribute added,
# * fixes syntax error due to a change in the Ruby interpreter. [Akinori MUSHA ]
# * debianization
#
#
# [0.3 07/04/2002]
#
# * fetch() replaced by fetch_net() however i created an alias to fetch()
# * fetch_cgi() added
# * discid read-only attribute added
# * free() bug on FreeBSD fixed in get_cdrom() [Stephane D'Alu ]
# * get_cdrom() buffer overrun fixed [OGAWA Takaya ]
#
#
# [0.2 19/01/2002]
#
# * Big cleaning of code.
# * Minimum code ( just the CDROM access ) written in C. Other is in pure Ruby.
# * Module now called 'freedb' instead of 'Freedb'.
# * Deleted specific exceptions. There is only one now (FreedbError).
#
#
# [0.1 18/12/2001]
#
# * Initial version
#
# License:: GPL
# Author:: Guillaume Pierronnet (mailto:moumar@netcourrier.com)
# Website:: http://davedd.free.fr/ruby-freedb/
# Raised on any kind of error related to ruby-freedb (cd-rom, network, protocol)
class FreedbError < StandardError ; end
class Freedb
VERSION = "0.5"
PROTO_LEVEL = 5
CD_FRAME = 75
VALID_CATEGORIES = [ "blues", "classical", "country", "data", "folk", "jazz", "misc", "newage", "reggae", "rock", "soundtrack" ]
# cddbid of the CD
attr_reader(:discid)
# the complete string used to query the database
attr_reader(:query)
# total length of the CD
attr_reader(:length)
# an array with all possible results for this CD
attr_reader(:results)
# string containing raw entry from freedb database
attr_reader(:raw_response)
# artist of the CD, must not be empty
attr_accessor(:artist)
# title of the CD, must not be empty
attr_accessor(:title)
# freedb category, must be one of +Freedb::VALID_CATEGORIES+
attr_accessor(:category)
# arbitraty string for the genre
attr_accessor(:genre)
# year of the cd (0 if not known)
attr_accessor(:year)
# an array of hashs containing following keys:
# "title" (must not be empty), "length", "ext" (for extended infos)
attr_accessor(:tracks)
# extended infos of the CD
attr_accessor(:ext_infos)
# If +is_query+ is false, the discid of the CD in +param+ is dumped.
# Else +param+ is considered as a valid freedb query string and is used directly.
def initialize(param = "/dev/cdrom", is_query = false)
@query =
if is_query
param
else
require "freedb_cdrom"
get_cdrom(param)
end
q = @query.split(" ")
@discid = q[0]
nb_tracks = q[1].to_i
@length = q[-1].to_i
@offsets = q[2...-1] << @length*CD_FRAME
@offsets.collect! { |x| x.to_i }
@tracks = Array.new
nb_tracks.times { |i|
t = Hash.new
t["length"] = ((@offsets[i+1]-@offsets[i]).to_f/CD_FRAME).round
@tracks << t
}
@revision = 0
@raw_response = ""
end
# Query database using network
# Fill the +results+ array with multiple results.
# return nil if no match found
def fetch_net(server = "freedb.org", port = 8880)
@handler = FetchNet.new(server, port)
_fetch
end
alias :fetch :fetch_net
# Query database using CGI (HTTP) method.
# Fill the +results+ array with multiple results.
# return nil if no match found
def fetch_cgi(server = "www.freedb.org", port = 80, proxy = nil, proxy_port = nil, path = "/~cddb/cddb.cgi")
@handler = FetchCGI.new(server, port, proxy, proxy_port, path)
_fetch
end
# Query database using local directory. Set +win_format+ to true
# if the database has windows format (see freedb howto in "misc/" for details)
# return nil if no match found
def fetch_disk(directory, win_format = false)
@handler = FetchDisk.new(directory, win_format)
_fetch
end
# submit the current Freedb object using http
# +from+ is an email adress used to return submissions errors
# +submit_mode+ can be set to "test" to check submission validity (for developpers)
# return nil
def submit_http(from = "user@localhost", server = "freedb.org", port = 80, path = "/~cddb/submit.cgi", submit_mode = "submit")
require "net/http"
headers = {
"Category" => @category,
"Discid" => @discid,
"User-Email" => from,
"Submit-Mode" => submit_mode,
"Charset" => "ISO-8859-1",
"X-Cddbd-Note" => "Sent by ruby-freedb #{VERSION}"
}
Net::HTTP.start(server, port) { |http|
reply, body = http.post(path, submit_body(), headers)
if reply.code != 200
raise(FreedbError, "Bad response from server: '#{body.chop}'")
end
}
nil
end
alias :submit :submit_http
# submit the current Freedb object using smtp
# return +nil+
def submit_mail(smtp_server, from = "localuser@localhost", port = 25, to = "freedb-submit@freedb.org")
# +to+ can be set to "test-submit@freedb.org" to check validity (for
# developpers)
require "net/smtp"
header = {
"From" => from,
"To" => to,
"Subject" => "cddb #{@category} #{@discid}",
"MIME-Version" => "1.0",
"Content-Type" => "text/plain",
"Content-Transfer-Encoding" => "quoted-printable",
"X-Cddbd-Note" => "Sent by ruby-freedb #{VERSION}"
}
msg = ""
header.each { |k, v|
msg << "#{k}: #{v}\r\n"
}
msg << "\r\n"
msg << submit_body
Net::SMTP.start(smtp_server, port) { |smtp| smtp.send_mail(msg, from, to) }
nil
end
# Retrieve full result from the database.
# If +index+ is a Fixnum, get the +index+'th result in the +result+ array
# If +index+ is a String, +index+ is the freedb category
def get_result(index)
if index.is_a?(String)
idx = nil
@results.each_with_index { |r, i|
if r =~ /^#{index}/
idx = i
end
}
else
idx = index
end
md = /^\S+ [0-9a-fA-F]{8}/.match(@results[idx])
@handler.send_cmd("read", md[0])
# swallow the whole response into a hash
response = Hash.new
each_line(@handler) { |line|
@raw_response << line + "\n"
case line
when /^(\d+) (\S+)/, /^([A-Za-z0-9_]+)=(.*)/
key = $1.upcase
val = $2.gsub(/\\(.)/) {
case $1
when "t"
"\t"
when "n"
"\n"
else
$1
end
}
(response[key] ||= '') << val
when /^# Revision: (\d+)/
@revision = $1.to_i
end
}
@category = response['210']
@genre = response['DGENRE']
@year = response['DYEAR'].to_i
@ext_infos = response['EXTD']
# Use a regexp instead of a bare string to avoid ruby >= 1.7 warning
@artist, @title = response['DTITLE'].split(/ \/ /, 2)
# A self-titled album may not have a title part
@title ||= @artist
response.each { |key, val|
case key
when /^4\d\d$/
raise(FreedbError, val)
when /^TTITLE(\d+)$/
i = $1.to_i
@tracks[i]["title"] = val
when /^EXTT(\d+)$/
i = $1.to_i
@tracks[i]["ext"] = val
end
}
self
end
# close all pending connections
def close
@handler.close if @handler
@handler = nil
end
private
def _fetch
@handler.gets #banner
#@handler.send_cmd("hello", "#{ENV['USER']} #{`hostname`.chop} ruby-freedb #{VERSION}")
@handler.send_cmd("hello", "user localhost ruby-freedb #{VERSION}")
if @handler.gets.chop =~ /^4\d\d (.+)/ #welcome
raise(FreedbError, $1)
end
set_proto_level(PROTO_LEVEL)
@handler.send_cmd("query", @query)
resp = @handler.gets.chop
@results = []
case resp
when /^200 (.+)/ #single result
@results << $1
when /^211/ #multiple results
each_line(@handler) { |l|
@results << l
}
when /^202/ #no match found
return nil
end
self
end
def set_proto_level(l)
if l < 1
raise(FreedbError, "Server doesn't support level 1!")
end
@handler.send_cmd("proto", l.to_s)
if @handler.gets =~ /^501/
set_proto_level(l-1)
end
end
def each_line(handler)
until (l = handler.gets) =~ /^\./
yield l.chop
end
end
def submit_body
if @tracks.detect { |h| h["title"].empty? }
raise(FreedbError, "Some tracks title are empty")
elsif not VALID_CATEGORIES.include?(@category)
raise(FreedbError, "Category is not valid")
elsif @artist.empty?
raise(FreedbError, "Artist field must not be empty")
elsif @title.empty?
raise(FreedbError, "Title field must not be empty")
end
body = < 1
@res << "211 Multiple match found"
match.each { |m|
@res << m
}
@res << "."
else
@res << "202 No match found"
end
when "read"
categ, discid = args.split(" ")
@res << "210 #{categ} #{discid}"
if @win
lines = @temp_results[categ]
else
filename = File.join(@basedir, categ, discid)
lines = File.readlines(filename)
end
@res.concat(lines.collect { |l| l.chop })
@res << "."
when "proto"
@res << "200 CDDB protocol level: current #{PROTO_LEVEL}, supported #{PROTO_LEVEL}"
else
@res << "501"
#$stderr.puts "#{self.class} unsupported command #{cmd}"
end
end
def gets
@res.shift + "\n"
end
def close; end
private
def disc_name(content)
disc_name = nil
content.each { |line|
if md = /DTITLE=(.+)/.match(line)
disc_name = $1
end
}
disc_name
end
def find_files_win(discid)
ret = []
head = discid[0, 2]
@categs.each do |dir, categ|
Dir.foreach(dir) do |filename|
if filename =~ /^([0-9a-fA-f]{2})to([0-9a-fA-F]{2})$/ and head >= $1 and head <= $2
ret << [File.join(dir, filename), categ]
end
end
end
ret
end
end
end