Initial rev of superrip
[cmccabe-bin] / superrip.rb
1 #!/usr/bin/ruby -w
2
3 #
4 # superrip.rb
5 #
6 # Advanced CD-ROM ripping program
7
8 # Copyright 2010, Colin McCabe
9 #
10
11 require 'fileutils'
12 require 'optparse'
13 require 'optparse/time'
14 require 'ostruct'
15
16 #-----------------------------------------------------------------
17 # constants
18 #-----------------------------------------------------------------
19 $cd_dev = "/dev/cdrom"
20
21 #-----------------------------------------------------------------
22 # functions
23 #-----------------------------------------------------------------
24 def my_system(cmd)
25   puts cmd
26   system(cmd) unless $opts.dry_run
27   ($?.exitstatus == 0) or raise "#{cmd} failed"
28 end
29
30 def die_unless_installed(cmd)
31   system("which #{cmd} > /dev/null")
32   ($?.exitstatus == 0) or raise "you need to install the #{cmd} program"
33 end
34
35 def get_number_of_tracks_on_cd
36   look_for_tracks = false
37   IO.popen("cdda2wav -v summary -J dev=#{$cd_dev}", "r") do |io|
38     line = io.read.chomp
39     if (line =~ /^AUDIOtrack/) then
40       look_for_tracks = true
41     elsif (look_for_tracks == true) then
42       look_for_tracks = false
43       line =~ /[ \t]*1-([1234567890][1234567890]*)[^1234567890]/ \
44         or raise "couldn't understand cdda2wav output!"
45       return $1.to_i
46     end
47   end
48   raise "couldn't find what we were looking for in cdda2wav output!"
49 end
50
51 def audiorip(track, number)
52   begin
53     my_system("nice -1 cdparanoia -w -d #{$cd_dev} #{number}")
54   rescue
55     raise "failed to rip track #{number} (#{track.name})"
56   end
57   # cdparanoia always outputs to cdda.wav
58   FileUtils.mv("cdda.wav", track.wav_file_name, $fu_args)
59
60   # TODO: spawn a thread to do this stuff in the background
61   FileUtils.mkdir_p(track.flac_dir, $fu_args)
62   my_system("flac -c #{track.wav_file_name} > #{track.flac_file_name}")
63   my_system("lame -q 1 -b 192 #{track.wav_file_name} > #{track.mp3_file_name}")
64   FileUtils.rm_f(track.wav_file, $fu_args)
65 end
66
67 #-----------------------------------------------------------------
68 # classes
69 #-----------------------------------------------------------------
70 class MyOptions
71   def self.parse(args)
72     opts = OpenStruct.new
73     opts.dry_run = false
74     $fu_args = { :verbose => true }
75
76     # Fill in opts values
77     parser = OptionParser.new do |myparser|
78       myparser.banner = "Usage: #{ File.basename($0) } [opts]"
79       myparser.separator("Specific options:")
80       myparser.on("--dry-run", "-d",
81             "Show what would be done, without doing it.") do |a|
82         opts.dry_run = true
83         $fu_args = { :verbose => true, :noop => true }
84       end
85       myparser.on("--tracklist", "-t",
86             "Provide a list of tracks to use.") do |file|
87         opts.manifest_file = file
88         opts.partial = false
89       end
90       myparser.on("--partial-tracklist", "-T",
91             "Provide a partial list of tracks to use.") do |file|
92         opts.manifest_file = file
93         opts.partial = true
94       end
95     end
96     parser.parse!(args)
97
98     raise "you must provide a tracklist" unless opts.manifest_file != nil
99     return opts
100   end
101 end
102
103 class Track
104   attr_accessor :name, :flac_dir, :flac_file_name, :mp3_dir, :mp3_file_name
105   def initialize(name)
106     if name =~ /\[LL\]/ then
107       raise "you can't include [LL] in a track name" 
108     end
109     if name =~ /\.mp3/ then
110       raise "don't include .mp3 in the track name; that will be added"
111     end
112     if name =~ /\.flac/ then
113       raise "don't include .flac in the track name; that will be added"
114     end
115     (name =~ /([^\/][^\/]*)\/([^\/]*[^\/])/) or \
116       raise "track name must be of the form 'foo/bar'"
117     @name = name
118     @flac_dir = "#{1} [LL]"
119     @flac_file_name = "#{@flac_dir}/#{2}.flac"
120     @mp3_dir = "#{1}"
121     @mp3_file_name = "#{@mp3_dir}/#{2}.mp3"
122     @wav_file_name = "#{1}__#{2}.wav"
123   end
124 end
125
126 class Manifest
127   def initialize(filename)
128     @tracks = Hash.new
129     eval(filename)
130     @tracks.each do |key, val|
131       @tracks[key] = Track.new(val)
132     end
133   end
134
135   def validate(num_tracks)
136     if (@tracks.empty?) then
137       raise "you must define some tracks"
138     end
139     @tracks.each { |t| t.validate }
140     if (not $opts.partial) then
141       (1..num_tracks).each do |t|
142         if not @tracks[t].defined?
143           raise "don't know what to do with track #{t}"
144         end
145       end
146     end
147   end
148
149   def rip(num_tracks)
150     (1..num_tracks).each do |t|
151       next unless @tracks.defined?(t)
152       audiorip(t)
153     end
154   end
155 end
156
157 #-----------------------------------------------------------------
158 # main
159 #-----------------------------------------------------------------
160 # Parse options.
161 begin
162   begin
163     $opts = MyOptions.parse(ARGV)
164   rescue ArgumentError => msg
165   $stderr.puts("#{msg} Type --help to see usage information.\n")
166   exit 1
167   end
168 end
169
170 die_unless_installed("lame")
171 die_unless_installed("flac")
172 die_unless_installed("cdparanoia")
173 die_unless_installed("cdda2wav")
174
175 manifest = Manifest.new($opts.manifest_file)
176 num_tracks = get_number_of_tracks_on_cd()
177 puts "num_tracks = #{num_tracks}"
178 #manifest.rip(num_tracks)
179 exit 0