6 # Advanced CD-ROM ripping program
8 # Copyright 2010, Colin McCabe
13 require 'optparse/time'
16 #-----------------------------------------------------------------
18 #-----------------------------------------------------------------
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
38 IO.popen("cdda2wav -v summary -J dev=#{$opts.cd_dev} 2>&1", "r") do |io|
39 io.readlines.each do |line|
42 if (line =~ /^AUDIOtrack/) then
43 look_for_tracks = true
44 elsif (look_for_tracks == true) then
45 look_for_tracks = false
46 line =~ /[ \t]*1-([ 1234567890][1234567890]*)[^1234567890]/ \
47 or raise "couldn't understand cdda2wav output! (line:#{line})"
52 raise "couldn't find what we were looking for in cdda2wav output! \
53 output:#{lines.join('\n')}"
56 # Process the WAV file into an MP3 and FLAC file.
57 # This is done in a background process.
58 def process_wav(track)
59 FileUtils.mkdir_p(track.flac_dir, verbose: true, noop: !$opts.dry_run)
60 my_system("flac -f '#{track.wav_file_name}' \
61 --output-name='#{track.flac_file_name}' &>/dev/null")
62 my_system("flac --test '#{track.flac_file_name}' &>/dev/null")
63 FileUtils.mkdir_p(track.mp3_dir, verbose: true, noop: !$opts.dry_run)
64 my_system("lame -q 1 -b 192 '#{track.wav_file_name}' \
65 '#{track.mp3_file_name}' &>/dev/null")
66 FileUtils.rm_f(track.wav_file_name, verbose: true, noop: !$opts.dry_run)
69 def audiorip(tnum, track)
71 my_system("nice -1 cdparanoia -w -d #{$opts.cd_dev} #{tnum}")
73 raise "failed to rip track #{tnum} (#{track.name})"
75 # cdparanoia always outputs to cdda.wav
76 FileUtils.mv("cdda.wav", track.wav_file_name, verbose: true, noop: !$opts.dry_run)
78 # If there are too many processes, wait for one of them to terminate
79 if ($children.keys.length > $opts.max_children) then
80 pid, status = Process.wait2(-1)
81 if (status.exitstatus != 0) then
82 raise "process #{pid} failed with exitstatus #{status.exitstatus}"
93 puts "*** FATAL ERROR: #{e}"
102 #-----------------------------------------------------------------
104 #-----------------------------------------------------------------
107 opts = OpenStruct.new
109 opts.max_children = 4
110 opts.cd_dev = "/dev/cdrom"
112 # Fill in opts values
113 parser = OptionParser.new do |myparser|
114 myparser.banner = "Usage: #{ File.basename($0) } [opts]"
115 myparser.separator("Specific options:")
116 myparser.on("--dev [DEV]", "-D",
117 "choose the cdrom device file to use") do |dev|
120 myparser.on("--dry-run", "-d",
121 "Show what would be done, without doing it.") do |a|
124 myparser.on("--tracklist [FILE]", "-t",
125 "Provide a list of tracks to use.") do |file|
126 opts.manifest_file = file
129 myparser.on("--partial-tracklist [FILE]", "-T",
130 "Provide a partial list of tracks to use.") do |file|
131 opts.manifest_file = file
134 myparser.on("--max-children [NCHILD]", "-j",
135 "The maximum number of child processes to allow at any \
136 given time") do |nchild|
137 opts.max_children = nchild.to_i
138 if (opts.max_children < 1) then
139 raise "can't set max_children to #{opts.max_children}"
145 raise "you must provide a tracklist" unless opts.manifest_file != nil
151 attr_accessor :name, :flac_dir, :flac_file_name, :mp3_dir, :mp3_file_name,
154 if name =~ /\[LL\]/ then
155 raise "you can't include [LL] in a track name"
157 if name =~ /\.mp3/ then
158 raise "don't include .mp3 in the track name; that will be added"
160 if name =~ /\.flac/ then
161 raise "don't include .flac in the track name; that will be added"
163 (name =~ /([^\/][^\/]*)\/([^\/]*[^\/])/) or \
164 raise "track name must be of the form 'foo/bar'"
166 @flac_dir = "#{$1} [LL]"
167 @flac_file_name = "#{@flac_dir}/#{$2}.flac"
169 @mp3_file_name = "#{@mp3_dir}/#{$2}.mp3"
170 @wav_file_name = "#{$1}__#{$2}.wav"
174 "track(\"#{@name}\")"
179 def initialize(filename)
181 eval(File.new(filename).read)
182 @t.each do |key, val|
183 @t[key] = Track.new(val)
185 # TODO: implement some shortcuts that make manifests easier to type.
186 # Probably avoiding the necessity to continue typing the album name if it is the same as the
187 # previous track's name would make things a lot easier without complicating everything too much.
190 def validate(num_tracks)
192 raise "you must define some tracks"
194 if ($opts.partial) then
195 highest_track = @t.keys.sort[-1]
196 if (num_tracks < highest_track) then
197 raise "can't rip track #{highest_track}, because there are \
198 only #{num_tracks} tracks"
201 (1..num_tracks).each do |tnum|
202 if not @t.has_key?(tnum)
203 raise "don't know what to do with track #{tnum}"
207 # TODO: make sure that tracks inside albums are in order
208 # i.e. we don't map track 2 to a name that sorts to before track 1
212 (1..num_tracks).each do |tnum|
213 next unless @t.has_key?(tnum)
214 audiorip(tnum, @t[tnum])
216 prc = Process.waitall
218 if (pair[1].exitstatus != 0) then
219 raise "process #{pair[0]} failed with exitstatus #{pair[1].exitstatus}"
226 @t.keys.sort.each do |key|
227 ret = "#{ret}#{key}:'#{@t[key].inspect()}'\n"
233 #-----------------------------------------------------------------
235 #-----------------------------------------------------------------
239 $opts = MyOptions.parse(ARGV)
240 rescue ArgumentError => msg
241 $stderr.puts("#{msg} Type --help to see usage information.\n")
246 die_unless_installed("lame")
247 die_unless_installed("flac")
248 die_unless_installed("cdparanoia")
249 die_unless_installed("cdda2wav")
251 manifest = Manifest.new($opts.manifest_file)
252 puts manifest.inspect
253 num_tracks = get_number_of_tracks_on_cd()
254 puts "found #{num_tracks} tracks"
255 manifest.validate(num_tracks)
256 manifest.rip(num_tracks)
257 puts "*** FINISHED ***"