Added locale_from_utf8, dropped the glib dependency.
[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 'iconv'
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 def locale_from_utf8(s)
180         # FIXME: Encoding is only available in Ruby 1.9
181         to_charset = Encoding.locale_charmap
182         utf8 = 'UTF-8'.freeze
183
184         if to_charset == utf8
185                 s.dup
186         else
187                 Iconv.iconv to_charset, utf8, s
188         end
189 end
190
191 cfg_file = ARGV.first || File.expand_path("~/.rbrip.yaml")
192
193 begin
194         config = YAML.load(File.open(cfg_file))
195 rescue
196         config = OpenStruct.new(
197                 :device => "/dev/hdd",
198                 :destdir => ".",
199                 :dirmask => "%a/%y-%l",
200                 :filemask => "%n-%a-%t",
201                 :sanitize_filenames => true,
202                 :vorbis_quality => 5
203         )
204
205         File.open(cfg_file, "w") { |f| YAML.dump(config, f) }
206 end
207
208 mb = MusicBrainz::Client.new
209 mb.device = config.device
210
211 unless mb.query(MusicBrainz::Query::GetCDInfo)
212         STDERR.puts "Error: " + mb.error
213         exit 1
214 end
215
216 num_albums = mb.result(MusicBrainz::Query::GetNumAlbums).to_i
217
218 if num_albums < 1
219         STDERR.puts "Album not found. Guess you need to submit it:"
220         puts mb.url
221         exit 1
222 elsif num_albums > 1
223         STDERR.puts "Multiple albums found. What now?"
224         exit 1
225 end
226
227 mb.select(MusicBrainz::Query::Rewind)
228 mb.select(MusicBrainz::Query::SelectAlbum, 1)
229
230 album = Album.new(mb)
231
232 if album.id == ""
233         STDERR.puts "Album not found. Guess you need to submit it:"
234         puts mb.url
235         exit 1
236 end
237
238 puts "Disc info:\n
239  Artist: #{album.artist.name.rpad(20)} (#{album.artist.id})
240  Name:   #{album.name.rpad(20)} (#{album.id})
241
242  Tracks:\n"
243
244 album.tracks.each_with_index do |t, i|
245         tmp = locale_from_utf8 t.name
246         puts "%3s." % (i + 1) + " #{tmp}"
247 end
248
249 print "\nContinue [Y/n]? "
250 STDOUT.flush
251 exit if STDIN.getc == ?n
252
253 print "\nEnter year of release: "
254 STDOUT.flush
255
256 album.year = STDIN.gets.strip.to_i
257
258 print "Enter genre: "
259 STDOUT.flush
260
261 album.genre = STDIN.gets.strip
262
263 # where to put the encoded files?
264 destdir = config.dirmask.dup
265
266 dir_map = {
267         "%A" => album.artist.name.sanitize,
268         "%L" => album.name.sanitize,
269         "%Y" => album.year.to_s
270 }
271
272 if config.sanitize_filenames
273         dir_map.each_value { |v| v.sanitize! }
274 end
275
276 dir_map.each do |k, v|
277         destdir.gsub!(/#{k}/, v)
278         destdir.gsub!(/#{k.downcase}/, v.downcase)
279 end
280
281 destdir = File.join(config.destdir, destdir)
282 FileUtils.makedirs(destdir) unless File.directory?(destdir)
283
284 puts "\nRipping to #{destdir}\n\n"
285
286 files = []
287 enc = VorbisEncoder.new(config)
288 #enc = FlacEncoder.new(config)
289
290 metadata = {
291         :artist => album.artist.name,
292         :artist_id => album.artist.id,
293         :disc => album.name,
294         :disc_id => album.id,
295         :genre => album.genre,
296         :year => album.year
297 }
298 file_map = dir_map.dup
299
300 album.tracks.each_with_index do |t, i|
301         t = album.tracks[i]
302
303         track_no = i + 1
304         file = config.filemask.dup
305
306         file_map["%N"] = "%02i" % track_no
307
308         if config.sanitize_filenames
309                 file_map["%T"] = t.name.sanitize
310         else
311                 file_map["%T"] = t.name
312         end
313
314         metadata[:track_no] = track_no.to_s
315         metadata[:track_name] = t.name
316         metadata[:track_id] = t.id
317
318         file_map.each do |k, v|
319                 file.gsub!(/#{k}/, v)
320                 file.gsub!(/#{k.downcase}/, v.downcase)
321         end
322
323         if config.sanitize_filenames
324                 if file[-1] == ?.
325                         file = file[0..-2]
326                 end
327         end
328
329         file = File.join(destdir, file)
330         files << file
331
332         rip_cmd = "cdparanoia -q -d \"#{config.device}\" -w -- #{track_no} -"
333         encode_cmd = enc.encode(file + enc.suffix, metadata)
334
335         puts "Ripping: #{track_no}. #{t.name} => #{file + enc.suffix}"
336         `#{rip_cmd} | #{encode_cmd}`
337
338         puts "Applying tags..."
339         enc.apply_tags(file + enc.suffix, metadata)
340 end
341
342 rplgain_cmd = enc.compute_replaygain(files)
343
344 unless rplgain_cmd.nil?
345         puts "Computing replaygain..."
346         `#{rplgain_cmd} > /dev/null`
347 end