6 # Advanced CD-ROM ripping program
8 # Copyright 2010, Colin McCabe
13 require 'optparse/time'
16 #-----------------------------------------------------------------
18 #-----------------------------------------------------------------
19 $cd_dev = "/dev/cdrom"
21 #-----------------------------------------------------------------
23 #-----------------------------------------------------------------
26 system(cmd) unless $opts.dry_run == true
27 ($?.exitstatus == 0) or raise "#{cmd} failed"
30 def die_unless_installed(cmd)
31 system("which #{cmd} > /dev/null")
32 ($?.exitstatus == 0) or raise "you need to install the #{cmd} program"
35 def get_number_of_tracks_on_cd
36 look_for_tracks = false
37 IO.popen("cdda2wav -v summary -J dev=#{$cd_dev} 2>&1", "r") do |io|
38 io.readlines.each do |line|
40 if (line =~ /^AUDIOtrack/) then
41 look_for_tracks = true
42 elsif (look_for_tracks == true) then
43 look_for_tracks = false
44 line =~ /[ \t]*1-([1234567890][1234567890]*)[^1234567890]/ \
45 or raise "couldn't understand cdda2wav output!"
50 raise "couldn't find what we were looking for in cdda2wav output!"
53 def audiorip(tnum, track)
55 my_system("nice -1 cdparanoia -w -d #{$cd_dev} #{tnum}")
57 raise "failed to rip track #{tnum} (#{track.name})"
59 # cdparanoia always outputs to cdda.wav
60 FileUtils.mv("cdda.wav", track.wav_file_name, $fu_args)
62 # TODO: spawn a thread to do this stuff in the background
63 FileUtils.mkdir_p(track.flac_dir, $fu_args)
64 my_system("flac -f '#{track.wav_file_name}' --output-name='#{track.flac_file_name}'")
66 my_system("flac --test '#{track.flac_file_name}'")
68 raise "failed to encode #{track.flac_file_name}"
70 FileUtils.mkdir_p(track.mp3_dir, $fu_args)
71 my_system("lame -q 1 -b 192 '#{track.wav_file_name}' '#{track.mp3_file_name}'")
72 FileUtils.rm_f(track.wav_file_name, $fu_args)
75 #-----------------------------------------------------------------
77 #-----------------------------------------------------------------
82 $fu_args = { :verbose => true }
85 parser = OptionParser.new do |myparser|
86 myparser.banner = "Usage: #{ File.basename($0) } [opts]"
87 myparser.separator("Specific options:")
88 myparser.on("--dry-run", "-d",
89 "Show what would be done, without doing it.") do |a|
91 $fu_args = { :verbose => true, :noop => true }
93 myparser.on("--tracklist [FILE]", "-t",
94 "Provide a list of tracks to use.") do |file|
95 opts.manifest_file = file
98 myparser.on("--partial-tracklist [FILE]", "-T",
99 "Provide a partial list of tracks to use.") do |file|
100 opts.manifest_file = file
106 raise "you must provide a tracklist" unless opts.manifest_file != nil
112 attr_accessor :name, :flac_dir, :flac_file_name, :mp3_dir, :mp3_file_name,
115 if name =~ /\[LL\]/ then
116 raise "you can't include [LL] in a track name"
118 if name =~ /\.mp3/ then
119 raise "don't include .mp3 in the track name; that will be added"
121 if name =~ /\.flac/ then
122 raise "don't include .flac in the track name; that will be added"
124 (name =~ /([^\/][^\/]*)\/([^\/]*[^\/])/) or \
125 raise "track name must be of the form 'foo/bar'"
127 @flac_dir = "#{$1} [LL]"
128 @flac_file_name = "#{@flac_dir}/#{$2}.flac"
130 @mp3_file_name = "#{@mp3_dir}/#{$2}.mp3"
131 @wav_file_name = "#{$1}__#{$2}.wav"
135 "track(\"#{@name}\")"
140 def initialize(filename)
142 eval(File.new(filename).read)
143 @t.each do |key, val|
144 @t[key] = Track.new(val)
146 # TODO: implement some shortcuts that make manifests easier to type.
147 # Probably avoiding the necessity to continue typing the album name if it is the same as the
148 # previous track's name would make things a lot easier without complicating everything too much.
151 def validate(num_tracks)
153 raise "you must define some tracks"
155 @t.each { |t| t.validate }
156 if (not $opts.partial) then
157 (1..num_tracks).each do |t|
158 if not @t[t].defined?
159 raise "don't know what to do with track #{t}"
163 # TODO: make sure that tracks inside albums are in order
164 # i.e. we don't map track 2 to a name that sorts to before track 1
168 (1..num_tracks).each do |tnum|
169 next unless @t.has_key?(tnum)
170 audiorip(tnum, @t[tnum])
176 @t.keys.sort.each do |key|
177 ret = "#{ret}#{key}:'#{@t[key].inspect()}'\n"
183 #-----------------------------------------------------------------
185 #-----------------------------------------------------------------
189 $opts = MyOptions.parse(ARGV)
190 rescue ArgumentError => msg
191 $stderr.puts("#{msg} Type --help to see usage information.\n")
196 die_unless_installed("lame")
197 die_unless_installed("flac")
198 die_unless_installed("cdparanoia")
199 die_unless_installed("cdda2wav")
201 manifest = Manifest.new($opts.manifest_file)
202 puts manifest.inspect
203 num_tracks = get_number_of_tracks_on_cd()
204 puts "found #{num_tracks} tracks"
205 manifest.rip(num_tracks)