Initial commit.
[rb-rip.git] / rb-rip
1 #!/usr/bin/ruby -w
2
3 require 'fileutils'
4 require "musicbrainz"
5 require "yaml"
6 require "ostruct"
7 #require "glib2"
8 require "tempfile"
9
10 class Track
11         attr_reader :id, :name
12
13         def initialize(mb)
14                 @name = mb.result(MusicBrainz::Query::TrackGetTrackName)
15
16                 tmp = mb.result(MusicBrainz::Query::TrackGetTrackId)
17                 @id = mb.id_from_url(tmp)
18         end
19 end
20
21 class Artist
22         attr_reader :id, :name
23
24         def initialize(mb)
25                 tmp = mb.result(MusicBrainz::Query::AlbumGetAlbumArtistId)
26                 @id = mb.id_from_url(tmp)
27
28                 @name = mb.result(MusicBrainz::Query::AlbumGetArtistName, 1)
29         end
30 end
31
32 class Album
33         attr_reader :id, :name, :artist, :tracks
34         attr_accessor :genre, :year
35
36         def initialize(mb)
37                 @genre = nil
38                 @year = nil
39
40                 @artist = Artist.new(mb)
41
42                 tmp = mb.result(MusicBrainz::Query::AlbumGetAlbumId)
43                 @id = mb.id_from_url(tmp)
44
45                 @name = mb.result(MusicBrainz::Query::AlbumGetAlbumName)
46
47                 num_tracks = mb.result(MusicBrainz::Query::AlbumGetNumTracks).to_i
48                 @tracks = Array.new(num_tracks)
49
50                 1.upto(num_tracks) do |j|
51                         mb.select(MusicBrainz::Query::SelectTrack, j)
52
53                         @tracks[j - 1] = Track.new(mb)
54
55                         mb.select(MusicBrainz::Query::Back)
56                 end
57         end
58 end
59
60 class Encoder
61         def initialize(config)
62                 @cfg = config
63         end
64
65         def encode(dest, metadata)
66         end
67
68         def apply_tags(dest, metadata)
69         end
70
71         def compute_replaygain
72         end
73
74         def suffix
75         end
76 end
77
78 class VorbisEncoder < Encoder
79         def encode(dest, m)
80                 <<EOF
81                 oggenc -q #{@cfg.vorbis_quality} -Q -o "#{dest}" -
82 EOF
83         end
84
85         def apply_tags(dest, m)
86                 Tempfile.open("rbrip.vorbiscomments") do |tf|
87                         tf.puts("artist=" + m[:artist])
88                         tf.puts("title=" + m[:track_name])
89                         tf.puts("album=" + m[:disc])
90                         tf.puts("tracknumber=" + m[:track_no])
91                         tf.puts("genre=" + m[:genre])
92                         tf.puts("date=" + m[:year].to_s)
93                         tf.puts("musicbrainz_albumid=" + m[:disc_id])
94                         tf.puts("musicbrainz_artistid=" + m[:artist_id])
95                         tf.puts("musicbrainz_trackid=" + m[:track_id])
96                         tf.flush
97
98                         FileUtils.mv(dest, dest + ".untagged")
99                         `vorbiscomment -w "#{dest}.untagged" "#{dest}" -c #{tf.path}`
100                         FileUtils.rm(dest + ".untagged")
101                 end
102         end
103
104         def compute_replaygain(files)
105                 "vorbisgain -a " + files.map { |f| f + suffix }.join(" ")
106         end
107
108         def suffix
109                 ".ogg"
110         end
111 end
112
113 class FlacEncoder < Encoder
114         def encode(dest, m)
115                 s =<<EOF
116 flac --no-ogg
117   --sign=signed --endian=little --channels=2 --bps=16 --sample-rate=44100
118   -o "#{dest}" -
119 EOF
120
121                 s.gsub!(/\n/, " ")
122                 s
123         end
124
125         def apply_tags(dest, m)
126                 Tempfile.open("rbrip.vorbiscomments") do |tf|
127                         tf.puts("artist=" + m[:artist])
128                         tf.puts("title=" + m[:track_name])
129                         tf.puts("album=" + m[:disc])
130                         tf.puts("tracknumber=" + m[:track_no])
131                         tf.puts("genre=" + m[:genre])
132                         tf.puts("date=" + m[:year].to_s)
133                         tf.puts("musicbrainz_albumid=" + m[:disc_id])
134                         tf.puts("musicbrainz_artistid=" + m[:artist_id])
135                         tf.puts("musicbrainz_trackid=" + m[:track_id])
136                         tf.flush
137
138                         `metaflac --import-tags-from=#{tf.path} #{dest}`
139                 end
140         end
141
142         def compute_replaygain(files)
143                 "metaflac --add-replay-gain " + files.map { |f| f + suffix }.join(" ")
144         end
145
146         def suffix
147                 ".flac"
148         end
149 end
150
151 class String
152         def sanitize
153                 # cut leading and trailing "..."
154                 if self[0..2] == "..."
155                         tmp = self[3..-1]
156                 else
157                         tmp = self.dup
158                 end
159
160                 if tmp[-3..-1] == "..."
161                         tmp = tmp[0..-4]
162                 end
163
164                 tmp.gsub(/\s+/, "_").gsub(/&/, "and").delete(",'()[]/\\!?\":").
165                 tr("-", "_").
166                 squeeze(" ").squeeze(".").squeeze("_")
167         end
168
169         def sanitize!
170                 replace(sanitize)
171         end
172
173         def rpad(total)
174                 n = [total - length, 0].max
175                 self + (" " * n)
176         end
177 end
178
179 cfg_file = ARGV.first || File.expand_path("~/.rbrip.yaml")
180
181 begin
182         config = YAML.load(File.open(cfg_file))
183 rescue
184         config = OpenStruct.new(
185                 :device => "/dev/hdd",
186                 :destdir => ".",
187                 :dirmask => "%a/%y-%l",
188                 :filemask => "%n-%a-%t",
189                 :sanitize_filenames => true,
190                 :vorbis_quality => 5
191         )
192
193         File.open(cfg_file, "w") { |f| YAML.dump(config, f) }
194 end
195
196 mb = MusicBrainz::Client.new
197 mb.device = config.device
198
199 unless mb.query(MusicBrainz::Query::GetCDInfo)
200         STDERR.puts "Error: " + mb.error
201         exit 1
202 end
203
204 num_albums = mb.result(MusicBrainz::Query::GetNumAlbums).to_i
205
206 if num_albums < 1
207         STDERR.puts "Album not found. Guess you need to submit it:"
208         puts mb.url
209         exit 1
210 elsif num_albums > 1
211         STDERR.puts "Multiple albums found. What now?"
212         exit 1
213 end
214
215 mb.select(MusicBrainz::Query::Rewind)
216 mb.select(MusicBrainz::Query::SelectAlbum, 1)
217
218 album = Album.new(mb)
219
220 if album.id == ""
221         STDERR.puts "Album not found. Guess you need to submit it:"
222         puts mb.url
223         exit 1
224 end
225
226 puts "Disc info:\n
227  Artist: #{album.artist.name.rpad(20)} (#{album.artist.id})
228  Name:   #{album.name.rpad(20)} (#{album.id})
229
230  Tracks:\n"
231
232 album.tracks.each_with_index do |t, i|
233         #tmp = GLib.locale_from_utf8(t.name)
234         tmp = t.name
235         puts "%3s." % (i + 1) + " #{tmp}"
236 end
237
238 print "\nContinue [Y/n]? "
239 STDOUT.flush
240 exit if STDIN.getc == ?n
241
242 print "\nEnter year of release: "
243 STDOUT.flush
244
245 album.year = STDIN.gets.strip.to_i
246
247 print "Enter genre: "
248 STDOUT.flush
249
250 album.genre = STDIN.gets.strip
251
252 # where to put the encoded files?
253 destdir = config.dirmask.dup
254
255 dir_map = {
256         "%A" => album.artist.name.sanitize,
257         "%L" => album.name.sanitize,
258         "%Y" => album.year.to_s
259 }
260
261 if config.sanitize_filenames
262         dir_map.each_value { |v| v.sanitize! }
263 end
264
265 dir_map.each do |k, v|
266         destdir.gsub!(/#{k}/, v)
267         destdir.gsub!(/#{k.downcase}/, v.downcase)
268 end
269
270 destdir = File.join(config.destdir, destdir)
271 FileUtils.makedirs(destdir) unless File.directory?(destdir)
272
273 puts "\nRipping to #{destdir}\n\n"
274
275 files = []
276 enc = VorbisEncoder.new(config)
277 #enc = FlacEncoder.new(config)
278
279 metadata = {
280         :artist => album.artist.name,
281         :artist_id => album.artist.id,
282         :disc => album.name,
283         :disc_id => album.id,
284         :genre => album.genre,
285         :year => album.year
286 }
287 file_map = dir_map.dup
288
289 album.tracks.each_with_index do |t, i|
290         t = album.tracks[i]
291
292         track_no = i + 1
293         file = config.filemask.dup
294
295         file_map["%N"] = "%02i" % track_no
296
297         if config.sanitize_filenames
298                 file_map["%T"] = t.name.sanitize
299         else
300                 file_map["%T"] = t.name
301         end
302
303         metadata[:track_no] = track_no.to_s
304         metadata[:track_name] = t.name
305         metadata[:track_id] = t.id
306
307         file_map.each do |k, v|
308                 file.gsub!(/#{k}/, v)
309                 file.gsub!(/#{k.downcase}/, v.downcase)
310         end
311
312         if config.sanitize_filenames
313                 if file[-1] == ?.
314                         file = file[0..-2]
315                 end
316         end
317
318         file = File.join(destdir, file)
319         files << file
320
321         rip_cmd = "cdparanoia -q -d \"#{config.device}\" -w -- #{track_no} -"
322         encode_cmd = enc.encode(file + enc.suffix, metadata)
323
324         puts "Ripping: #{track_no}. #{t.name} => #{file + enc.suffix}"
325         `#{rip_cmd} | #{encode_cmd}`
326
327         apltags = enc.apply_tags(file + enc.suffix, metadata)
328         puts "Applying tags..."
329         `#{apltags} > /dev/null`
330 end
331
332 rplgain_cmd = enc.compute_replaygain(files)
333
334 unless rplgain_cmd.nil?
335         puts "Computing replaygain..."
336         `#{rplgain_cmd} > /dev/null`
337 end