· 9 years ago · Oct 11, 2016, 11:58 AM
1#!/usr/bin/env ruby
2# Rubyripper - A secure ripper for Linux/BSD/OSX
3# Copyright (C) 2007 Bouke Woudstra (rubyripperdev@gmail.com)
4#
5# This file is part of Rubyripper. Rubyripper is free software: you can
6# redistribute it and/or modify it under the terms of the GNU General
7# Public License as published by the Free Software Foundation, either
8# version 3 of the License, or (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program. If not, see <http://www.gnu.org/licenses/>
17
18#ensure translation is working before actually installing
19LOCALE=[ENV['PWD'] + "/locale", "/usr/share/locale"]
20LOCALE.each{|dir| if File.directory?(dir) ; ENV['GETTEXT_PATH'] = dir ; break end}
21
22$rr_version = '0.6.2' #application wide setting
23
24begin
25 require 'gettext'
26 include GetText
27 bindtextdomain("rubyripper")
28rescue LoadError
29 puts "ruby-gettext is not found. Translations are disabled!"
30 def _(txt)
31 txt
32 end
33end
34
35Thread.abort_on_exception = true
36
37require 'monitor' #help library for threaded applications
38require 'yaml' #help library to save data structures into files
39require 'fileutils' #help library for moving files
40
41def installed(filename) # a help function to check if an application is installed
42 ENV['PATH'].split(':').each do |dir|
43 if File.exist?(dir + '/' + filename) ; return true end
44 end
45 if File.exist?(filename) ; return true else return false end #it can also be in current working dir
46end
47
48if not installed('cdparanoia')
49 puts "Cdparanoia not found on your system.\nThis is required to run rubyripper. Exiting..."
50 exit()
51end
52
53def get_example_filename_normal(basedir, layout) #separate function to make it faster
54 filename = File.join(basedir, layout)
55 filename = File.expand_path(filename)
56 filename = _("Example filename: %s.ext") % [filename]
57 {'%a' => 'Judas Priest', '%b' => 'Sin After Sin', '%f' => 'codec', '%g' => 'Rock', '%y' => '1977', '%n' =>'01', '%t' => 'Sinner', '%i' =>'inputfile', '%o' => 'outputfile'}.each{|key, value| filename.gsub!(key,value)}
58 return filename
59end
60
61def get_example_filename_various(basedir, layout) #separate function to make it faster
62 filename = File.join(basedir, layout)
63 filename = File.expand_path(filename)
64 filename = _("Example filename: %s.ext") % [filename]
65 {'%va' => 'Various Artists', '%b' => 'TMF Rockzone', '%f' => 'codec', '%g' => "Rock", '%y' => '1999', '%n' => '01', '%a' => 'Kid Rock', '%t' => 'Cowboy'}.each{|key, value| filename.gsub!(key,value)}
66 return filename
67end
68
69def eject(cdrom)
70 Thread.new do
71 if installed('eject') ; `eject #{cdrom}`
72 elsif installed('diskutil'); `diskutil eject #{cdrom}` #Mac users don't got eject, but diskutil
73 else puts _("No eject utility found!")
74 end
75 end
76end
77
78class Settings
79attr_reader :settings, :configFound
80 def initialize(configFile = false)
81 @settings = Hash.new()
82 @configFound = false
83 @defaultSettings = {"flac" => false, #boolean
84 "flacsettings" => "--best -V", #string, passed to flac
85 "vorbis" => true, #boolean
86 "vorbissettings" => "-q 4", #string, passed to vorbis
87 "mp3" => false, #boolean
88 "mp3settings" => "-V 3 --id3v2-only", #string, passed to lame
89 "wav" => false, #boolean
90 "other" => false, #boolean, any other codec
91 "othersettings" => '', #string, the complete command
92 "playlist" => true, #boolean
93 "cdrom" => cdrom_drive(), #string
94 "offset" => 0, #integer
95 "maxThreads" => 2, #integer, number of encoding proces while ripping
96 "rippersettings" => '', #string, passed to cdparanoia
97 "max_tries" => 5, #integer, #tries before giving up correcting
98 'basedir' => '~/', #string, where to store your new rips?
99 'naming_normal' => '%f/%a (%y) %b/%n - %t', #string, normal discs
100 'naming_various' => '%f/%va (%y) %b/%n - %a - %t', #string various
101 'naming_image' => '%f/%a (%y) %b/%a - %b (%y)', #string, image
102 "verbose" => false, #boolean, extra verbose info shown in terminal
103 "debug" => true, #boolean, extra debug info shown in terminal
104 "eject" => true, #boolean, open up the tray when finished
105 'ripHiddenAudio' => true, #boolean, rip part before track 1
106 'minLengthHiddenTrack' => 2, #integer, min. length hidden track
107 "req_matches_errors" => 2, # #integer, matches when errors detected
108 "req_matches_all" => 2, #integer, #matches when no errors detected
109 "site" => "http://freedb.freedb.org/~cddb/cddb.cgi", #string, freedb site
110 "username" => "anonymous", #string, user name freedb
111 "hostname" => "my_secret.com", #string, hostname freedb
112 "first_hit" => true, #boolean, always choose 1st option
113 "freedb" => true, #boolean, enable freedb
114 "editor" => editor(), #string, default editor
115 "filemanager" => filemanager(), #string, default file manager
116 "browser" => browser(), #string, default browser
117 "no_log" =>false, #boolean, delete log if no errors?
118 "create_cue" => true, #boolean, create cuesheet
119 "image" => false, #boolean, save to single file
120 'normalize' => false, #boolean, normalize volume?
121 'gain' => "album", #string, gain mode
122 'gainTagsOnly' => false, #string, not actually modify audio
123 'noSpaces' => false, #boolean, replace spaces with underscores
124 'noCapitals' => false, #boolean, replace uppercase with lowercase
125 'pregaps' => "prepend", #string, way to handle pregaps
126 'preEmphasis' => 'cue' #string, way to handle pre-emphasis
127 }
128 setFileLocation(configFile)
129 migrationCheck()
130 loadSettings()
131 end
132
133 # set all file locations
134 def setFileLocation(configFile)
135 if configFile == false
136 dir = ENV['XDG_CONFIG_HOME'] || File.join(ENV['HOME'], '.config')
137 @configFile = File.join(dir, 'rubyripper/settings')
138 else
139 @configFile = File.expand_path(configFile)
140 end
141
142 dir = ENV['XDG_CACHE_HOME'] || File.join(ENV['HOME'], '.cache')
143 @cacheFile = File.join(dir, 'rubyripper/freedb.yaml')
144
145 #store the location in the settings for use later on
146 @defaultSettings['freedbCache'] = @cacheFile
147
148 createDirs(File.dirname(@configFile))
149 createDirs(File.dirname(@cacheFile))
150 end
151
152 # help function to create dirs
153 def createDirs(dirName)
154 if !File.directory?(File.dirname(dirName))
155 createDirs(File.dirname(dirName))
156 end
157 Dir.mkdir(dirName) if !File.directory?(dirName)
158 end
159
160 # check for existing configs in old directories and move them
161 # to the standard directories conform the freedesktop.org spec
162 def migrationCheck()
163 oldDir = File.join(ENV['HOME'], '.rubyripper')
164 if File.directory?(oldDir)
165 puts "Auto migrating to new file locations..."
166 oldConfig = File.join(ENV['HOME'], '.rubyripper/settings')
167 oldCache = File.join(ENV['HOME'], '.rubyripper/freedb.yaml')
168 moveFiles(oldConfig, oldCache)
169 deleteOldDir(oldDir)
170 end
171
172 # clean up a very old config file
173 if File.exists?(oldFile = File.join(ENV['HOME'], '.rubyripper_settings'))
174 FileUtils.rm(oldFile)
175 end
176 end
177
178 # help function to move the files
179 def moveFiles(oldConfig, oldCache)
180 if File.exists?(oldConfig)
181 FileUtils.mv(oldConfig, @configFile)
182 puts "New location of config file: #{@configFile}"
183 end
184
185 if File.exists?(oldCache)
186 FileUtils.mv(oldCache, @cacheFile)
187 puts "New location of freedb file: #{@cacheFile}"
188 end
189 end
190
191 # help function to remove the old dir
192 def deleteOldDir(oldDir)
193 if File.symlink?(oldDir)
194 File.delete(oldDir)
195 elsif Dir.entries(oldDir).size > 2 # some leftover file(s) remain
196 puts "#{oldDir} could not be removed: it's not empty!"
197 else
198 Dir.delete(oldDir)
199 end
200 puts "Auto migration finished succesfull."
201 end
202
203 # first load defaults, then overwrite the values found in the config file
204 def loadSettings()
205 @settings = @defaultSettings.dup
206
207 if File.exist?(@configFile)
208 @configFound = true
209 file = File.new(@configFile,'r')
210 while line = file.gets
211 key, value = line.split('=', 2)
212 # remove the trailing newline character
213 value.rstrip!
214 # replace the strings false/true with a bool
215 if value == "false" ; value = false
216 elsif value == "true" ; value = true
217 # replace two quotes with an empty string
218 elsif value == "''" ; value = ''
219 # replace an integer string with an integer
220 elsif value.to_i > 0 || value == '0' ; value = value.to_i
221 end
222 # only load a setting that is included in the default
223 if @defaultSettings.key?(key) ; @settings[key] = value end
224 end
225 file.close()
226 end
227 end
228
229 # save the settings to the config file
230 def save(settings)
231 file = File.new(@configFile, 'w')
232 settings.each do |key, value|
233 file.puts "#{key}=#{value}" if @defaultSettings.include?(key)
234 end
235 file.close()
236 end
237
238 # determine default drive
239 def cdrom_drive #default values for cdrom drives under differenty os'es
240 drive = 'Unknown!'
241 system = RUBY_PLATFORM
242 if system.include?('openbsd')
243 drive = '/dev/cd0c' # as provided in issue 324
244 elsif system.include?('linux') || system.include?('bsd')
245 drive = '/dev/cdrom'
246 elsif system.include?('darwin')
247 drive = '/dev/disk1'
248 end
249 return drive
250 end
251
252 # determine default file manager
253 def filemanager #look for default filemanager
254 if ENV['DESKTOP_SESSION'] == 'kde' && installed('dolphin')
255 return 'dolphin'
256 elsif ENV['DESKTOP_SESSION'] == 'kde' && installed('konqueror')
257 return 'konqueror'
258 elsif installed('thunar')
259 return 'thunar' #Xfce4 filemanager
260 elsif installed('nautilus')
261 return 'nautilus --no-desktop' #Gnome filemanager
262 else
263 return 'echo'
264 end
265 end
266
267 # determine default editor
268 def editor # look for default editor
269 if ENV['DESKTOP_SESSION'] == 'kde' && installed('kwrite')
270 return 'kwrite'
271 elsif installed('mousepad')
272 return 'mousepad' #Xfce4 editor
273 elsif installed('gedit')
274 return 'gedit' #Gnome editor
275 elsif ENV.key?('EDITOR')
276 return ENV['EDITOR']
277 else
278 return 'echo'
279 end
280 end
281
282 #determine default browser
283 def browser
284 if installed('chromium')
285 return 'chromium'
286 elsif ENV['DESKTOP_SESSION'] == 'kde' && installed('konqueror')
287 return 'konqueror'
288 elsif installed('epiphany')
289 return 'epiphany'
290 elsif installed('firefox')
291 return 'firefox'
292 elsif installed('opera')
293 return 'opera'
294 elsif ENV.key?('BROWSER')
295 return ENV['BROWSER']
296 else
297 return 'echo'
298 end
299 end
300end
301
302class Gui_support
303attr_reader :rippingErrors, :encodingErrors, :short_summary
304attr_writer :encodingErrors
305
306 def initialize(settings) #gui is an instance of the graphical user interface used
307 @settings = settings
308 createLog()
309
310 @problem_tracks = Hash.new # key = tracknumber, value = new dictionary with key = seconds_chunk, value = [amount_of_chunks, trials_needed]
311 @not_corrected_tracks = Array.new # Array of tracks that weren't corrected within the maximum amount of trials set by the user
312 @ripping_progress = 0.0
313 @encoding_progress = 0.0
314 @encodingErrors = false
315 @rippingErrors = false
316 @short_summary = _("Artist : %s\nAlbum: %s\n") % [@settings['cd'].md.artist, @settings['cd'].md.album]
317 addLog(_("This log is created by Rubyripper, version %s\n") % [$rr_version])
318 addLog(_("Website: http://code.google.com/p/rubyripper\n\n"))
319 end
320
321 def createLog
322 @logfiles = Array.new
323 ['flac', 'vorbis', 'mp3', 'wav', 'other'].each do |codec|
324 if @settings[codec]
325 @logfiles << File.open(@settings['Out'].getLogFile(codec), 'a')
326 end
327 end
328 end
329
330 # update the ripping percentage of the gui
331 def ripPerc(new_value, calling_function = false) #new_value = float, 1 = 100%
332 new_value <= 1.0 ? @ripping_progress = new_value : @ripping_progress = 1.0
333 @settings['instance'].update("ripping_progress", @ripping_progress)
334 end
335
336 # update the encoding percentage of the gui
337 def encPerc(new_value, calling_function = false) #new_value = float, 1 = 100%
338 new_value <= 1.0 ? @encoding_progress = new_value : @encoding_progress = 1.0
339 @settings['instance'].update("encoding_progress", @encoding_progress)
340 end
341
342 # Add a message to the logging file + update the gui
343 def add(message, calling_function = false)
344 @logfiles.each{|logfile| logfile.print(message); logfile.flush()} # Append the messages to the logfiles
345 @settings['instance'].update("log_change", message)
346 end
347
348 # Add a message to the logging file
349 def addLog(message, summary = false)
350 @logfiles.each{|logfile| logfile.print(message); logfile.flush()} # Append the messages to the logfiles
351 if summary ; @short_summary += message end
352 end
353
354 def mismatch(track, trial, indexes_with_errors, size, length)
355 if !@problem_tracks.key?(track) #First time we encounter this track (Secure_rip->analyzeFiles() )
356 @problem_tracks[track] = Hash.new # create the Hash for the track
357 indexes_with_errors.each do |index_of_chunk|
358 seconds = index_of_chunk / 176400 # position of chunk rounded in seconds, each second = 176400 bytes
359 if !@problem_tracks[track].key?(seconds)
360 @problem_tracks[track][seconds] = [1, trial] # different_chunks, trial. First time we encounter this position, so different_chunks = 1
361 else
362 @problem_tracks[track][seconds][0] += 1 # one more chunk at the same second
363 end
364 end
365 else
366 indexes_with_errors.each do |index_of_chunk|
367 seconds = index_of_chunk / 176400 # position of chunk rounded in seconds, each second = 176400 bytes
368 @problem_tracks[track][seconds][1] = trial #Update the amount of trials needed
369 end
370 end
371 if trial == 0; @not_corrected_tracks << track end #Reached maxtries and still got errors
372 end
373
374 def summary(matches_all, matches_errors, maxtries) #Give an overview of errors
375 if @encodingErrors ; addLog(_("\nWARNING: ENCODING ERRORS WERE DETECTED\n"), true) end
376 addLog(_("\nRIPPING SUMMARY\n\n"), true)
377
378 addLog(_("All chunks were tried to match at least %s times.\n") % [matches_all], true)
379 if matches_all != matches_errors; addLog(_("Chunks that differed after %s trials,\nwere tried to match %s times.\n") % [matches_all, matches_errors], true) end
380
381 if @problem_tracks.empty?
382 addLog(_("None of the tracks gave any problems\n"), true)
383 elsif @not_corrected_tracks.size != 0
384 addLog(_("Some track(s) could NOT be corrected within the maximum amount of trials\n"), true)
385 @not_corrected_tracks.each do |track|
386 @rippingErrors = true
387 addLog(_("Track %s could NOT be corrected completely\n") % [track], true)
388 end
389 else
390 addLog(_("Some track(s) needed correction,but could\nbe corrected within the maximum amount of trials\n"), true)
391 end
392
393 if !@problem_tracks.empty? # At least some correction was necessary
394 position_analyse(matches_errors, maxtries)
395 @short_summary += _("The exact positions of the suspicious chunks\ncan be found in the ripping log\n")
396 end
397 @logfiles.each{|logfile| logfile.close} #close all the files
398 end
399
400 def position_analyse(matches_errors, maxtries) # Give an overview of suspicion position in the logfile
401 addLog(_("\nSUSPICIOUS POSITION ANALYSIS\n\n"))
402 addLog(_("Since there are 75 chunks per second, after making the notion of the\n"))
403 addLog(_("suspicious position, the amount of initially mismatched chunks for\nthat position is shown.\n\n"))
404 @problem_tracks.keys.sort.each do |track| # For each track show the position of the files, how many chunks of that position and amount of trials needed to solve
405 addLog(_("TRACK %s\n") % [track])
406 @problem_tracks[track].keys.sort.each do |length| #length = total seconds of suspicious position
407 minutes = length / 60 # ruby math -> 70 / 60 = 1 (how many times does 60 fit in 70)
408 seconds = length % 60 # ruby math -> 70 % 60 = 10 (leftover)
409 if @problem_tracks[track][length][1] != 0
410 addLog(_("\tSuspicious position : %s:%s (%s x) (CORRECTED at trial %s)\n") % [sprintf("%02d", minutes), sprintf("%02d", seconds), @problem_tracks[track][length][0], @problem_tracks[track][length][1] + 1])
411 else # Position could not be corrected
412 addLog(_("\tSuspicious position : %s:%s (%sx) (COULD NOT BE CORRECTED)\n") % [ sprintf("%02d", minutes), sprintf("%02d", seconds), @problem_tracks[track][length][0]])
413 end
414 end
415 end
416 end
417
418 # delete the logfiles if no errors occured
419 def delLog
420 if @problem_tracks.empty? && !@encodingErrors
421 @logfiles.each{|logfile| File.delete(logfile.path)}
422 end
423 end
424end
425
426class Disc
427attr_reader :cdrom, :multipleDriveSupport, :audiotracks, :devicename,
428:playtime, :freedbString, :oldFreedbString, :totalSectors, :md, :error,
429:discId, :toc, :tocStarted, :tocFinished
430
431 def initialize(settings, gui=false, oldFreedbString = '', test = false)
432 @settings = settings
433 @cdrom = @settings['cdrom']
434 @freedb = @settings['freedb']
435 @verbose = @settings['verbose']
436 @gui = @settings['instance']
437
438 @oldFreedbString = oldFreedbString #if new disc is the same, later on force a connection with the freedb server
439 setVariables()
440 if audioDisc()
441 getDiscInfo()
442 analyzeTOC() #table of contents
443 @md = Metadata.new(self, @settings)
444 prepareToc() unless test == true # use help of cdrdao to get info about pregaps etcetera
445 end
446 end
447
448 def setVariables
449 @multipleDriveSupport = true #not always the case on MacOS's cdparanoia
450
451 @audiotracks = 0
452 @lengthSector = Hash.new
453 @startSector = Hash.new
454 @lengthText = Hash.new
455 @devicename = _("Unknown drive")
456 @playtime = '00:00'
457
458 @firstAudioTrack = 1 # some discs (games for instance) start with a data part
459 @datatrack = false
460 @freedbString = ''
461 @discId = ''
462
463 @totalSectors = 0
464
465 @error = '' #set to the error messsage
466
467 @toc = nil # instance of the AdvancedToc class
468 @tocStarted = false # keeps track if the toc class is ever created
469 @tocFinished = false
470 @cdrdaoThread = nil # to later synchronize
471 @cue = nil # instance of the Cuesheet class
472 end
473
474 # use cdrdao to scan for exact pregaps, hidden tracks, pre_emphasis
475 def prepareToc
476 if @settings['create_cue'] && installed('cdrdao')
477 @cdrdaoThread = Thread.new{advancedToc()}
478 end
479
480 if @settings['create_cue'] && !installed('cdrdao')
481 puts "Cdrdao not found. Advanced TOC analysis / cuesheet is skipped."
482 @settings['create_cue'] = false # for further assumptions later on
483 end
484 end
485
486 # start the Advanced toc instance
487 def advancedToc
488 @tocStarted = true
489 @toc = AdvancedToc.new(@settings)
490 end
491
492 # update the Disc class with actual settings and make a cuesheet
493 def updateSettings(settings)
494 @settings = settings
495
496 # user may have enabled cuesheet after the disc was scanned
497 # @toc is still nil because the class isn't finished yet
498 prepareToc() if @tocStarted == false
499
500 # if the scanning thread is still active, wait for it to finish
501 @cdrdaoThread.join() if @cdrdaoThread != nil
502
503 # update the length of the sectors + the start of the tracks if we're prepending the gaps
504 # also for the image since this is more easy with the cuesheet handling
505 if @settings['pregaps'] == "prepend" || @settings['image']
506 prependGaps()
507 end
508
509 # only make a cuesheet when the toc class is there
510 @cue = Cuesheet.new(@settings, @toc) if @toc != nil
511 end
512
513 # prepend the gaps, so rewrite the toc info
514 # notice that cdparanoia appends by default
515 def prependGaps
516 (2..@audiotracks).each do |track|
517 pregap = @toc.getPregap(track)
518 @lengthSector[track - 1] -= pregap
519 @startSector[track] -= pregap
520 @lengthSector[track] += pregap
521 end
522
523 if @settings['debug']
524 puts "Debug info: gaps are now prepended"
525 puts "Startsector\tLengthsector"
526 (1..@audiotracks).each do |track|
527 puts "#{@startSector[track]}\t#{@lengthSector[track]}"
528 end
529 end
530 end
531
532 def audioDisc
533 unless checkDevice() #check if the cdrom device is real and has permissions right
534 return false
535 end
536
537 @query = `cdparanoia -d #{@cdrom} -vQ 2>&1`
538
539 unless genericDevice() #check permission of generic device if it exists
540 return false
541 end
542
543 if $?.success? ; return true end #cdparanoia returned no problems
544
545 if @query.include?("Unable to open disc")
546 @query = false
547 @error = _("No disc found in drive %s.\n\n"\
548 "Please put an audio disc in first...") %[@cdrom]
549 return false
550 end
551
552 if @query.include?('USAGE')
553 if @verbose
554 puts _("Perhaps cdparanoia doesn't support the drive parameter.\n Will retry with default drive")
555 end
556 @query = `cdparanoia -vQ 2>&1`
557 if $?.success?
558 @multipleDriveSupport = false
559 return true
560 else
561 return false
562 end
563 end
564 end
565
566 def checkDevice
567 while File.symlink?(@cdrom) #find the name of the device, not the symlink
568 link = File.readlink(@cdrom)
569 if (link.include?('..') || !link.include?('/'))
570 @cdrom = File.expand_path(File.join(File.dirname(@cdrom), link))
571 else
572 @cdrom = link
573 end
574 end
575
576 unless File.blockdev?(@cdrom) #is it a real device?
577 @error = _("Cdrom drive %s does not exist on your system!\n"\
578 "Please configure your cdrom drive first.") % [@cdrom]
579 @query = false
580 return false
581 end
582
583 unless (File.readable?(@cdrom) && File.writable?(@cdrom))
584 @error = _("You don't have read and write permission\n"\
585 "for device %s on your system! These permissions are\n"\
586 "necessary for cdparanoia to scan your drive.\n\n%s\n"\
587 "You might want to add yourself to the necessary group in /etc/group")\
588 %[@cdrom, "ls -l shows #{`ls -l #{@cdrom}`}"]
589 return false
590 end
591
592 return true
593 end
594
595 def genericDevice #looking for the character device (sata/scsi-only)
596 device = nil
597 if @query.include?('generic device: ')
598 @query.each do |line|
599 if line =~ /generic device: /
600 device = $'.strip() #the part after the match
601 break #end the loop
602 end
603 end
604 else
605 return true
606 end
607
608 unless ((File.chardev?(device) || File.blockdev?(device)) && File.readable?(device) && File.writable?(device))
609 permission = nil
610 if File.chardev?(device) && installed('ls')
611 permission = `ls -l #{device}`
612 end
613
614 @error = _("You don't have read and write permission\n"\
615 "for device %s on your system! These permissions are\n"\
616 "necessary for cdparanoia to scan your drive.\n\n%s\n"\
617 "You might want to add yourself to the necessary group in /etc/group")\
618 %[device, "#{if permission ; "ls -l shows #{permission}" end}"]
619
620 return false
621 end
622
623 return true
624 end
625
626 def getDiscInfo
627 @query.split("\n").each do |line|
628 if line[0,5] =~ /\s+\d+\./
629 @audiotracks += 1
630 tracknumber, lengthSector, lengthText, startSector = line.split
631 @firstAudioTrack = tracknumber[0..-2].to_i if @audiotracks == 1
632 @lengthSector[@audiotracks] = lengthSector.to_i
633 @startSector[@audiotracks] = startSector.to_i
634 @lengthText[@audiotracks] = lengthText
635 elsif line =~ /CDROM\D*:/
636 @devicename = $'.strip()
637 elsif line[0,5] == "TOTAL"
638 @playtime = line.split()[2][1,5]
639 end
640 end
641
642 if @freedb ; getFreedbString end
643 @query = false
644 end
645
646 def getFreedbString
647 if installed('discid')
648 if RUBY_PLATFORM.include?('darwin') ; `diskutil unmount #{@cdrom}` end
649 @freedbString = `discid #{@cdrom}`
650 if RUBY_PLATFORM.include?('darwin') ; `diskutil mount #{@cdrom}` end
651 elsif installed('cd-discid') # if no discid exists, try cd-discid
652 @freedbString = `cd-discid #{@cdrom}`
653 else # if both do not exist generate it ourselves (less foolproof)
654 puts _("warning: discid or cd-discid isn't found on your system! Using fallback...")
655 checkDataTrack()
656 createFreedbString()
657 puts _("freedb string = %s" % [@freedbString])
658 end
659 @discId = @freedbString.split()[0]
660 end
661
662 def checkDataTrack
663 startSector = nil
664 lastSector = nil
665 if @query.include?('track_num = 176') # 176 = 0xb0 = end of data track??? # only MacOS version prints this
666 @query.split('/n').each do |line|
667 startSector = line.split()[5].to_i if (line[0,9] == "track_num" && startSector == nil)
668 lastSector = line.split()[5].to_i if line[0,15] == 'track_num = 176'
669 end
670 elsif installed('cd-info') #from the libcdio library
671 @query = `cd-info -C #{@cdrom}`
672 if @query.include?(' data ')
673 @query.split('/n').each do |line|
674 startSector = line.split()[2].to_i if (line =~ /\s+data\s+/ && startSector == nil)
675 lastSector = line.split()[2].to_i if line =~ /leadout/
676 end
677 end
678 end
679
680 if startSector != nil && lastSector != nil
681 puts "Data track is detected!"
682 @datatrack = [startSector, lastSector-startSector]
683 end
684 end
685
686 def createFreedbString
687 totalChecksum = 0
688 seconds = 0
689 freedbOffsets = ''
690 totalSectors = 0
691 audiotracks = @audiotracks
692 startSector = @startSector
693 lengthSector = @lengthSector
694
695 #I've commented this out since it actually hurts the results here
696 #if @datatrack
697 # audiotracks += 1 # take the datatrack in account
698 # startSector[audiotracks] = @datatrack[0] #add the start position of data track
699 # lengthSector[audiotracks] = @datatrack[1] #add the lenght of the data track
700 #end
701
702 (1..audiotracks).each do |track|
703 checksum = 0
704 seconds = (startSector[track] + 150) / 75 # MSF offset = 150
705 seconds.to_s.split(/\s*/).each{|s| checksum += s.to_i} # for example cddb sum of 338 seconds = 3+3+8=14
706 totalChecksum += checksum
707 end
708
709 totalSectors = (startSector[audiotracks] - startSector[1]) + lengthSector[audiotracks]
710 seconds = totalSectors / 75
711
712 discid = ((totalChecksum % 0xff) << 24 | seconds << 8 | audiotracks).to_s(16)
713 startSector.keys.sort.each{|track| freedbOffsets << (startSector[track] + 150).to_s + ' '}
714 @freedbString = "#{discid} #{audiotracks} #{freedbOffsets}#{(totalSectors + 150) / 75}" # MSF offset = 150
715 end
716
717 # When a data track is the first track on a disc, cdparanoia is acting strange:
718 # In the query it is showing as a start for 1s track the offset of the data track
719 # When ripping this offset isn't used however !! To allow a correct rip of this disc
720 # all startSectors have to be corrected. See also issue 196.
721
722 # If there is no data track at the start, but we do have an offset this means some
723 # hidden audio part. This part is marked as track 0. You can only assess this on
724 # a cd-player by rewinding from 1st track on.
725
726 def checkOffsetFirstTrack
727 if @firstAudioTrack != 1 # a disc that starts with data
728 dataOffset = @startSector[1]
729 @startSector.each_key{|track| @startSector[track] = @startSector[track] - dataOffset}
730 elsif @settings['ripHiddenAudio'] == false
731 #do nothing extra when hidden audio shouldn't be ripped
732 #in the cuesheet this part will be marked as a pregap (silence).
733 elsif @startSector[1] != 0 && @startSector[1] / 75.0 > @settings['minLengthHiddenTrack']
734 @startSector[0] = 0
735 @lengthSector[0] = @startSector[1]
736 elsif @startSector[1] != 0 # prepend the audio because it's not marked as a hidden track
737 @lengthSector[1] = @lengthSector[1] + @startSector[1]
738 @startSector[1] = 0
739 end
740 end
741
742 def analyzeTOC
743 checkOffsetFirstTrack()
744 @lengthSector.each_value{|track| @totalSectors += track}
745 end
746
747 # return the startSector, example for track 1 getStartSector(1)
748 def getStartSector(track)
749 if track == "image"
750 @startSector.key?(0) ? @startSector[0] : @startSector[1]
751 else
752 if @startSector.key?(track)
753 return @startSector[track]
754 else
755 return false
756 end
757 end
758 end
759
760 # return the sectors of the track, example for track 1 getLengthSector(1)
761 def getLengthSector(track)
762 if track == "image"
763 return @totalSectors
764 else
765 return @lengthSector[track]
766 end
767 end
768
769 # return the length of the track in text, example for track 1 getLengthSector(1)
770 def getLengthText(track)
771 if track == "image"
772 return @playtime
773 else
774 return @lengthText[track]
775 end
776 end
777
778 # return the length in bytes of the track, example for track 1 getFileSize(1)
779 def getFileSize(track)
780 if track == "image"
781 return 44 + @totalSectors * 2352
782 else
783 return 44 + @lengthSector[track] * 2352
784 end
785 end
786end
787
788
789# AdvancedToc is a class which helps detecting all special audio-cd
790# features as hidden tracks, pregaps, etcetera. It does so by
791# analyzing the output of cdrdao's TOC output. The class is only
792# opened when the user has the cuesheet enabled. This is so because
793# there is not much of an advantage of detecting pregaps when
794# they're just added to the file anyway. You want to detect
795# the gaps so you can reproduce the original disc exactly. The
796# cuesheet is necessary to store the gap info.
797
798class AdvancedToc
799attr_reader :log
800
801 def initialize(settings)
802 @settings = settings
803
804 setVariables()
805 readTOC()
806 end
807
808 # initialize all variables
809 def setVariables
810 @discType = "unknown"
811 @dataTracks = Array.new
812 @preEmphasis = Hash.new
813 @pregap = Hash.new
814 @silence = 0 #amount of sectors before the 1st track
815
816 @artist = String.new
817 @album = String.new
818 @tracknames = Hash.new
819
820 @index = 0
821 @toc = Array.new
822 @log = Array.new # saving the log messages
823
824 require 'tmpdir'
825 end
826
827 # get an output location for the temporary Toc file
828 def tocFile()
829 return File.join(Dir.tmpdir, "temp_#{File.basename(@settings['cdrom'])}.toc")
830 end
831
832 # fire the command to read the disc
833 def readTOC()
834 File.delete(tocFile()) if File.exist?(tocFile())
835 puts "Scanning disc with cdrdao" if @settings['debug']
836 `cdrdao read-toc --device #{@settings['cdrom']} \"#{tocFile()}\" #{"2>&1" if !@settings['verbose']}`
837 if $?.success?
838 parseTOC()
839 else
840 "cdrdao is killed."
841 end
842 end
843
844 # translate the file of cdrdao to a ruby array and interpret each line
845 def parseTOC
846 puts "Loading file: #{tocFile()}" if @settings['debug']
847 @toc = File.read(tocFile()).split("\n")
848 readDiscInfo()
849 readTrackInfo()
850
851 # give a message when no strange things are found
852 if @preEmphasis.empty? && @pregap.empty? && @silence == 0
853 @log << _("No pregaps, silences or pre-emphasis detected\n")
854 end
855
856 #set an extra whiteline before starting to rip
857 @log << "\n"
858
859 #might wanna fill the tags when otherwise Unknown is used
860 end
861
862 # parse the disc info
863 def readDiscInfo
864 @discType = @toc[0]
865 puts "Disc type = #{@discType}" if @settings['debug']
866
867 # continue reading the disc until the track info starts
868 @index = 1
869 while !@toc[@index].include?('//')
870 if @toc[@index].include?('CD_TEXT')
871 puts "Found cd_text for disc" if @settings['debug']
872 elsif @toc[@index].include?('TITLE')
873 @artist, @album = @toc[@index].strip().split(/\s\s+/)
874 @artist = @artist[6..-1] #remove TITLE
875 puts "Found artist for disc: #{@artist}" if @settings['debug']
876 puts "Found album for disc: #{@album}" if @settings['debug']
877 end
878 @index += 1
879 end
880 end
881
882 # parse the track info
883 def readTrackInfo
884 tracknumber = 0
885 while @index != @toc.size
886 if @toc[@index].include?('//')
887 tracknumber += 1
888 puts "Found info of tracknumber #{tracknumber}" if @settings['debug']
889 elsif @toc[@index].include?('TRACK DATA')
890 @dataTracks << tracknumber
891 @log << _("Track %s is marked as a DATA track\n") % [tracknumber]
892 elsif @toc[@index] == 'PRE_EMPHASIS'
893 @preEmphasis[tracknumber] = true
894 @log << _("Pre_emphasis detected on track %s\n") % [tracknumber]
895 elsif @toc[@index].include?('START')
896 sectorMinutes = 60 * 75 * @toc[@index][6..7].to_i
897 sectorSeconds = 75 * @toc[@index][9..10].to_i
898 @pregap[tracknumber] = sectorMinutes + sectorSeconds + @toc[@index][12..13].to_i
899 @log << _("Pregap detected for track %s : %s sectors\n") % [tracknumber, @pregap[tracknumber]]
900 elsif @toc[@index].include?('SILENCE')
901 sectorMinutes = 60 * 75 * @toc[@index][9..10].to_i
902 sectorSeconds = 75 * @toc[@index][11..12].to_i
903 @silence = sectorMinutes + sectorSeconds + @toc[@index][14..15].to_i
904 @log << _("Silence detected for track %s : %s sectors\n") % [tracknumber, @silence]
905 elsif @toc[@index].include?('TITLE')
906 @toc[@index] =~ /".*"/ #ruby's magical regular expressions
907 @tracknames[tracknumber] = $&[1..-2] #don't need the quotes
908 puts "CD-text found: Title = #{@tracknames[tracknumber]}" if @settings['debug']
909 end
910 @index += 1
911 end
912 end
913
914 # return the pregap if found, otherwise return 0
915 def getPregap(track)
916 if @pregap.key?(track)
917 return @pregap[track]
918 else
919 return 0
920 end
921 end
922
923 # return if a track has pre-emphasis
924 def hasPreEmph(track)
925 if @preEmphasis.key?(track)
926 return true
927 else
928 return false
929 end
930 end
931end
932
933#The Cuesheet class is there to provide a Cuesheet. It is
934#called from the Disc class after the toc scanning has
935#finished. There are several variants for building a cuesheet.
936#It at least needs a reference to all files. Single file is
937#the most simple, since the prepend / append discussion isn't
938#relevant here.
939#
940# NOTE Currently Data tracks are totally ignored for the cuesheet.
941# INFO -> TRACK 01 = Start point of track hh:mm:ff (h =hours, m = minutes, f = frames
942# INFO -> After each FILE entry should follow the format. Only WAVE and MP3 are allowed AND relevant.
943
944class Cuesheet
945 def initialize(settings, toc)
946 @settings = settings
947 @toc = toc
948 @filetype = {'flac' => 'WAVE', 'wav' => 'WAVE', 'mp3' => 'MP3', 'vorbis' => 'WAVE', 'other' => 'WAVE'}
949 allCodecs()
950 end
951
952 def allCodecs
953 ['flac','vorbis','mp3','wav','other'].each do |codec|
954 if @settings[codec]
955 @cuesheet = Array.new
956 @codec = codec
957 createCuesheet()
958 saveCuesheet()
959 end
960 end
961 end
962
963 def time(sector) # minutes:seconds:leftover frames
964 minutes = sector / 4500 # 75 frames/second * 60 seconds/minute
965 seconds = (sector % 4500) / 75
966 frames = sector % 75 # leftover
967 return "#{sprintf("%02d", minutes)}:#{sprintf("%02d", seconds)}:#{sprintf("%02d", frames)}"
968 end
969
970 def createCuesheet
971 @cuesheet << "REM GENRE #{@settings['Out'].genre}"
972 @cuesheet << "REM DATE #{@settings['Out'].year}"
973 @cuesheet << "REM COMMENT \"Rubyripper #{$rr_version}\""
974 @cuesheet << "REM DISCID #{@settings['cd'].discId}"
975 @cuesheet << "REM FREEDB_QUERY \"#{@settings['cd'].freedbString.chomp}\""
976 @cuesheet << "PERFORMER \"#{@settings['Out'].artist}\""
977 @cuesheet << "TITLE \"#{@settings['Out'].album}\""
978
979 # image rips should handle all info of the tracks at once
980 @settings['tracksToRip'].each do |track|
981 if track == "image"
982 writeFileLine(track)
983 (1..@settings['cd'].audiotracks).each{|audiotrack| trackinfo(audiotrack)}
984 else
985 if @toc.hasPreEmph(track) && (@settings['preEmphasis'] == 'cue' || !installed('sox'))
986 @cuesheet << "FLAGS PRE"
987 puts "Added PRE(emphasis) flag for track #{track}." if @settings['debug']
988 end
989
990 # do not put Track 00 AUDIO, but instead only mention the filename
991 if track == 0
992 writeFileLine(track)
993 # when a hidden track exists first enter the trackinfo, then the file
994 elsif track == 1 && @settings['cd'].getStartSector(0)
995 trackinfo(track)
996 writeFileLine(track)
997 # if there's a hidden track, start the first track at 0
998 @cuesheet << " INDEX 01 #{time(0)}"
999 # when no hidden track exists write the file and then the trackinfo
1000 elsif track == 1 && !@settings['cd'].getStartSector(0)
1001 writeFileLine(track)
1002 trackinfo(track)
1003 elsif @settings['pregaps'] == "prepend" || @toc.getPregap(track) == 0
1004 writeFileLine(track)
1005 trackinfo(track)
1006 else
1007 trackinfo(track)
1008 end
1009 end
1010 end
1011 end
1012
1013 #writes the location of the file in the Cue
1014 def writeFileLine(track)
1015 @cuesheet << "FILE \"#{File.basename(@settings['Out'].getFile(track, @codec))}\" #{@filetype[@codec]}"
1016 end
1017
1018 # write the info for a single track
1019 def trackinfo(track)
1020 @cuesheet << " TRACK #{sprintf("%02d", track)} AUDIO"
1021
1022 if track == 1 && @settings['ripHiddenAudio'] == false && @settings['cd'].getStartSector(1) > 0
1023 @cuesheet << " PREGAP #{time(@settings['cd'].getStartSector(1))}"
1024 end
1025
1026 @cuesheet << " TITLE \"#{@settings['Out'].getTrackname(track)}\""
1027 if @settings['Out'].getVarArtist(track) == ''
1028 @cuesheet << " PERFORMER \"#{@settings['Out'].artist}\""
1029 else
1030 @cuesheet << " PERFORMER \"#{@settings['Out'].getVarArtist(track)}\""
1031 end
1032
1033 trackindex(track)
1034 end
1035
1036 def trackindex(track)
1037 if @settings['image']
1038 # There is a different handling for track 1 and the rest
1039 if track == 1 && @settings['cd'].getStartSector(1) > 0
1040 @cuesheet << " INDEX 00 #{time(0)}"
1041 @cuesheet << " INDEX 01 #{time(@settings['cd'].getStartSector(track))}"
1042 elsif @toc.getPregap(track) > 0
1043 @cuesheet << " INDEX 00 #{time(@settings['cd'].getStartSector(track))}"
1044 @cuesheet << " INDEX 01 #{time(@settings['cd'].getStartSector(track) + @toc.getPregap(track))}"
1045 else # no pregap
1046 @cuesheet << " INDEX 01 #{time(@settings['cd'].getStartSector(track))}"
1047 end
1048 elsif @settings['pregaps'] == "append" && @toc.getPregap(track) > 0 && track != 1
1049 @cuesheet << " INDEX 00 #{time(@settings['cd'].getLengthSector(track-1) - @toc.getPregap(track))}"
1050 writeFileLine(track)
1051 @cuesheet << " INDEX 01 #{time(0)}"
1052 else
1053 # There is a different handling for track 1 and the rest
1054 # If no hidden audio track or modus is prepending
1055 if track == 1 && @settings['cd'].getStartSector(1) > 0 && !@settings['cd'].getStartSector(0)
1056 @cuesheet << " INDEX 00 #{time(0)}"
1057 @cuesheet << " INDEX 01 #{time(@toc.getPregap(track))}"
1058 elsif track == 1 && @settings['cd'].getStartSector(0)
1059 @cuesheet << " INDEX 01 #{time(0)}"
1060 elsif @settings['pregaps'] == "prepend" && @toc.getPregap(track) > 0
1061 @cuesheet << " INDEX 00 #{time(0)}"
1062 @cuesheet << " INDEX 01 #{time(@toc.getPregap(track))}"
1063 elsif track == 0 # hidden track needs index 00
1064 @cuesheet << " INDEX 00 #{time(0)}"
1065 else # no pregap or appended to previous which means it starts at 0
1066 @cuesheet << " INDEX 01 #{time(0)}"
1067 end
1068 end
1069 end
1070
1071 def saveCuesheet
1072 file = File.new(@settings['Out'].getCueFile(@codec), 'w')
1073 @cuesheet.each do |line|
1074 file.puts(line)
1075 end
1076 file.close()
1077 end
1078end
1079
1080class Metadata
1081attr_reader :status
1082attr_accessor :artist, :album, :genre, :year, :tracklist, :varArtists, :discNumber
1083
1084 def initialize(disc, settings)
1085 @disc = disc
1086 @gui = settings['instance']
1087 @verbose = settings['verbose']
1088 @settings = settings
1089 setVariables()
1090 end
1091
1092 def setVariables
1093 @artist = _('Unknown')
1094 @album = _('Unknown')
1095 @genre = _('Unknown')
1096 @year = '0'
1097 @discNumber = false
1098 @tracklist = Array.new
1099 @disc.audiotracks.times{|number| @tracklist << _("Track %s") % [number + 1]}
1100 @rawResponse = Array.new
1101 @choices = Array.new
1102 @varArtists = Array.new
1103 @varArtistsBackup = Array.new
1104 @backupTracklist = Array.new
1105 @status = false
1106 end
1107
1108 def freedb(freedbSettings, alwaysFirstChoice=true)
1109 @freedbSettings = freedbSettings
1110 @alwaysFirstChoice = alwaysFirstChoice
1111
1112 if not @disc.freedbString.empty? #no disc found
1113 searchMetadata()
1114 else
1115 @status = ["noAudioDisc", _("No audio disc found in %s") % [@cdrom]]
1116 end
1117 end
1118
1119 def searchMetadata
1120 if File.exist?(@settings['freedbCache'])
1121 @metadataFile = YAML.load(File.open(@settings['freedbCache']))
1122 #in case it got corrupted somehow
1123 @metadataFile = Hash.new if @metadataFile.class != Hash
1124 else
1125 @metadataFile = Hash.new
1126 end
1127
1128 if @disc.freedbString != @disc.oldFreedbString # Scanning the same disc will always result in an new freedb fetch.
1129 if @metadataFile.has_key?(@disc.freedbString) || findLocalMetadata #is the Metadata somewhere local?
1130 if @metadataFile.has_key?(@disc.freedbString)
1131 @rawResponse = @metadataFile[@disc.freedbString]
1132 end
1133 @tracklist.clear()
1134 handleResponse()
1135 @status = true # Give the signal that we're finished
1136 return true
1137 end
1138 end
1139
1140 if @verbose ; puts "preparing to contact freedb server" end
1141 handshake()
1142 end
1143
1144 def findLocalMetadata
1145 if File.directory?(dir = File.join(ENV['HOME'], '.cddb'))
1146 Dir.foreach(dir) do |subdir|
1147 if subdir == '.' || subdir == '..' || !File.directory?(File.join(dir, subdir)) ; next end
1148 Dir.foreach(File.join(dir, subdir)) do |file|
1149 if file == @disc.freedbString[0,8]
1150 puts "Local file found #{File.join(dir, subdir, file)}"
1151 # convert the string to an array, since ruby-1.9 handles these differently
1152 @rawResponse = File.read(File.join(dir, subdir, file)).split("\n")
1153 return true
1154 end
1155 end
1156 end
1157 end
1158 return false
1159 end
1160
1161 def handshake
1162 require 'net/http' #automatically loads the 'uri' library
1163 require 'cgi' #for communicating with the server
1164
1165 @url = URI.parse(@freedbSettings['site'])
1166
1167 if ENV['http_proxy']
1168 @proxy = URI.parse(ENV['http_proxy'])
1169 @server = Net::HTTP.new(@url.host, @url.port, @proxy.host,
1170 @proxy.port, @proxy.user,
1171 @proxy.password ? CGI.unescape(@proxy.password) : '')
1172 else
1173 @server = Net::HTTP.new(@url.host, @url.port)
1174 end
1175
1176 @query = @url.path + "?cmd=cddb+query+" + CGI.escape("#{@disc.freedbString.chomp}") + "&hello=" +
1177 CGI.escape("#{@freedbSettings['username']} #{@freedbSettings['hostname']} rubyripper #{$rr_version}") + "&proto=6"
1178 puts "cddb query 1 = #{@query}" if @settings['debug']
1179 if @verbose ; puts "Created query string: #{@query}" end
1180
1181 begin
1182 @answer = @server.get(@query).body
1183 requestDisc()
1184 rescue
1185 puts "Exception thrown: #{$!}"
1186 @status = ["networkDown", _("Couldn't connect to freedb server. Network down?\n\nDefault values will be shown...")]
1187 end
1188 end
1189
1190 def requestDisc # ask for matches on cd, if there are multiple, interaction with user is possible
1191 if @answer[0..2] == '200' #There was only one hit found
1192 if @verbose ; puts "One hit found; parsing" end
1193 temp, @category, @discid = @answer.split()
1194 freedbChoice()
1195 elsif @answer[0..2] == '211' || @answer[0..2] == '210' #Multiple hits were found
1196 multipleHits()
1197 @choices.each{|choice| puts "choice #{choice}"}
1198 if (@alwaysFirstChoice || @choices.length < 3) ; freedbChoice(0) #Always choose the first one
1199 else @status = ["choices", @choices] #Let the user choose
1200 end
1201 elsif @answer[0..2] == '202'
1202 @status = ["noMatches", _("No match in Freedb database. Default values are used.")]
1203 else
1204 @status = ["unknownReturnCode", _("cddb_query return code = %s. Return code not supported.") % [@answer[0..2]]]
1205 end
1206 end
1207
1208 def multipleHits
1209 discNames = @answer.split("\n")[1..@answer.length]; # remove the first line, which we know is the header
1210 discNames.each { |disc| @choices << disc.strip() unless (disc.strip() == "." || disc.strip().length == 0) }
1211 @choices << _("Keep defaults / don't use freedb") #also use the option to keep defaults
1212 end
1213
1214 def freedbChoice(choice=false)
1215 if choice != false
1216 if choice == @choices.size - 1 # keep defaults?
1217 @status = true
1218 return true
1219 end
1220 @category, @discid = @choices[choice].split
1221 end
1222 rawResponse()
1223 @tracklist.clear() #Now fill it with the real tracknames
1224 handleResponse()
1225 @status = true
1226 end
1227
1228 def rawResponse #Retrieve all usefull metadata into @rawResponse
1229 @query = @url.path + "?cmd=cddb+read+" + CGI.escape("#{@category} #{@discid}") + "&hello=" +
1230 CGI.escape("#{@freedbSettings['username']} #{@freedbSettings['hostname']} rubyripper #{$rr_version}") + "&proto=6"
1231 puts "cddb query 2 = #{@query}" if @settings['debug']
1232 if @verbose ; puts "Created fetch string: #{@query}" end
1233
1234 answer = @server.get(@query).body
1235 answers = answer.split("\n")
1236 answers.each do |line|
1237 line.chomp!
1238 @rawResponse << line unless (line == nil || line[-1,1] == '=' ||line[0,1] == '#' || line[0,1] == '.' )
1239 end
1240 saveResponse()
1241 end
1242
1243 def saveResponse
1244 if File.exist?(@settings['freedbCache'])
1245 @metadataFile = YAML.load(File.open(@settings['freedbCache']))
1246 else
1247 @metadataFile = Hash.new
1248 end
1249
1250 @metadataFile[@disc.freedbString] = @rawResponse
1251
1252 file = File.new(@settings['freedbCache'], 'w')
1253 file.write(@metadataFile.to_yaml)
1254 file.close()
1255 end
1256
1257 def saveChanges
1258 @rawResponse = Array.new
1259 @rawResponse << "DTITLE=#{@artist} \/ #{@album}"
1260 @rawResponse << "DYEAR=#{@year}"
1261 @rawResponse << "DGENRE=#{@genre}"
1262
1263 @disc.audiotracks.times do |index|
1264 if @varArtists.empty?
1265 @rawResponse << "TTITLE#{index}=#{@tracklist[index]}"
1266 else
1267 @rawResponse << "TTITLE#{index}=#{@varArtists[index]} / #{@tracklist[index]}"
1268 end
1269 end
1270 saveResponse()
1271 return true
1272 end
1273
1274 def handleResponse #Make some usefull variables from the raw_response.
1275 @rawResponse.each do |line|
1276 line.strip! #remove any newline characters
1277 if line =~ /DTITLE=/
1278 if @artist == _('Unknown') #first time we look at a DTITLE field (can have two lines at maximum)
1279 (@artist, @album) = $'.split(/\s+\/\s+/) # Remove the '/' with spaces around it, example DTITLE= Judas Priest / Sin After Sin
1280 if @artist == nil; @artist = _('Unknown') end
1281 if @album == nil; @album = _('Unknown') end
1282 elsif $' != nil # 2nd line with DTITLE, assume the second line is the continuation of the album name
1283 @album = "#{@album}#{$'}"
1284 end
1285 elsif line =~ /DYEAR=/
1286 @year = $' ; if @year == nil ; @year = 0 end
1287 elsif line =~ /DGENRE=/
1288 @genre = $' ; if @genre == nil ; @genre = _('Unknown') end
1289 elsif line =~ /TTITLE\d*=/
1290 trackname = $' # may also include variable artist
1291 line =~ /\d+/ ; tracknumber = $&.to_i #ruby magic, $& == the just matched regular expression
1292 if trackname != nil && @tracklist.empty? #1st track
1293 @tracklist << trackname
1294 elsif trackname != nil && @tracklist.length == tracknumber #counting of tracknumber starts with 0
1295 @tracklist << trackname
1296 elsif trackname != nil #already had a line for this track, so add this trackname to the previous one
1297 @tracklist[-1] += trackname
1298 end
1299 end
1300 end
1301 checkVarArtist()
1302 end
1303
1304#Various artist albums have different ways to show the artist and trackname
1305#Most common notation is ARTIST / TITLE
1306#Next to that is ARTIST - TITLE
1307#Then we got "TITLE" by ARTIST
1308# Some albums use a mixture of these schemes
1309
1310 def checkVarArtist
1311 sep = ''
1312 varArtist = false
1313 @disc.audiotracks.times do |tracknumber|
1314 if @tracklist[tracknumber] && @tracklist[tracknumber] =~ /[-\/]|\sby\s/
1315 varArtist = true
1316 else
1317 varArtist = false # one of the tracks does not conform to VA schema
1318 break # consider the whole album as not VA
1319 end
1320
1321 end
1322
1323 if varArtist == true
1324 @backupTracklist = @tracklist.dup() #backup before overwrite with new values
1325 @tracklist.each_index{|index| @varArtists[index], @tracklist[index] = @tracklist[index].split(/\s*[\/-]\s*|(\sby\s)\s*/)} #remove any spaces (\s) around sep
1326 end
1327 end
1328
1329 def undoVarArtist
1330 # first backup in case we want to revert back
1331 @varArtistsBackup = @varArtists.dup()
1332 @varTracklistBackup = @tracklist.dup()
1333
1334 # reset original values
1335 @varArtists = Array.new
1336
1337 # restore the tracklist
1338 @tracklist = @backupTracklist.dup
1339 end
1340
1341#reset to various artists when originally detected as such and made undone
1342 def redoVarArtist
1343 if !@backupTracklist.empty? && !@varArtistsBackup.empty?
1344 @tracklist = @varTracklistBackup
1345 @varArtists = @varArtistsBackup
1346 end
1347 end
1348end
1349
1350# Output is a helpclass that defines all the names of the directories,
1351# filenames and tags. It filters out special characters that are not
1352# well supported in the different platforms. It also offers some help
1353# functions to create the output dirs and to get a preview of the output.
1354# Since all the info is here, also create the playlist files. The cuesheets
1355# are also made with help of the Cuesheet class.
1356# Output is initialized as soon as the player pushes Rip Now!
1357
1358class Output
1359attr_reader :status, :artist, :album, :year, :genre
1360
1361 def initialize(settings)
1362 @settings = settings
1363 @md = @settings['cd'].md
1364 @codecs = ['flac', 'vorbis', 'mp3', 'wav', 'other']
1365 # Status of the class is false until proven otherwise
1366 @status = false
1367
1368 # the output of the dirs for each codec, and files for each tracknumber + codec.
1369 @dir = Hash.new
1370 @file = Hash.new
1371 @image = Hash.new
1372
1373 # the metadata made ready for tagging usage
1374 @artist = String.new
1375 @album = String.new
1376 @year = String.new
1377 @genre = String.new
1378 @tracklist = Hash.new
1379 @varArtists = Hash.new
1380 @otherExtension = String.new
1381
1382 splitDirFile()
1383 checkNames()
1384 setDirectory()
1385 attemptDirCreation()
1386 end
1387
1388 # split the filescheme into a dir and a file
1389 def splitDirFile
1390 if @settings['image']
1391 fileScheme = @settings['naming_image']
1392 elsif @md.varArtists.empty?
1393 fileScheme = @settings['naming_normal']
1394 else
1395 fileScheme = @settings['naming_various']
1396 end
1397
1398 # the basedir is added later on, since we don't want to change it
1399 @dirName, @fileName = File.split(fileScheme)
1400 end
1401
1402# Do a few sanity checks
1403# 1) Remove dot(s) from the albumname when it's the start of a directory,
1404# otherwise they're hidden files in linux.
1405# 2) Check if %va exists in filescheme for normal artists
1406# 3) Check if %n exists in single file rip scheme
1407# 4) Check if %va exists in single file rip scheme
1408# 5) Check if %t exists in single file rip scheme
1409
1410 def checkNames
1411 if @dirName.include?("/%b") && @md.album[0,1] == '.'
1412 @dirName.sub!(/\.*/, '')
1413 end
1414
1415 if @md.varArtists.empty? && @fileName.include?('%va')
1416 @fileName.gsub!('%va', '')
1417 puts "Warning: '%va' in the filescheme for normal cd's makes no sense!"
1418 puts "This is automatically removed"
1419 end
1420
1421 if @settings['image']
1422 if @fileName.include?('%n')
1423 @fileName.gsub!('%n', '')
1424 puts "Warning: '%n' in the filescheme for image rips makes no sense!"
1425 puts "This is automatically removed"
1426 end
1427
1428 if @fileName.include?('%va')
1429 @fileName.gsub!('%va', '')
1430 puts "Warning: '%va' in the filescheme for image rips makes no sense!"
1431 puts "This is automatically removed"
1432 end
1433
1434 if @fileName.include?('%t')
1435 @fileName.gsub!('%t', '')
1436 puts "Warning: '%t' in the filescheme for image rips makes no sense!"
1437 puts "This is automatically removed"
1438 end
1439 end
1440 end
1441
1442 # fill the @dir variable with all output dirs
1443 def setDirectory
1444 @codecs.each do |codec|
1445 if @settings[codec]
1446 @dir[codec] = giveDir(codec)
1447 end
1448 end
1449 end
1450
1451 # determine the output dir
1452 def giveDir(codec)
1453 dirName = @dirName.dup
1454
1455 # no forward slashes allowed in dir names
1456 @artistFile = @md.artist.gsub('/', '')
1457 @albumFile = @md.album.gsub('/', '')
1458
1459 # do not allow multiple directories for various artists
1460 {'%a' => @artistFile, '%b' => @albumFile, '%f' => codec, '%g' => @md.genre,
1461 '%y' => @md.year, '%va' => @artistFile}.each do |key, value|
1462 dirName.gsub!(key, value)
1463 end
1464
1465 if @md.discNumber != false
1466 dirName = File.join(dirName, "CD #{sprintf("%02d", @md.discNumber)}")
1467 end
1468
1469 dirName = fileFilter(dirName, true)
1470 dirName.force_encoding("UTF-8")
1471 return File.expand_path(File.join(@settings['basedir'], dirName))
1472 end
1473
1474 # (re)attempt creation of the dirs, when succesfull create the filenames
1475 def attemptDirCreation
1476 if not checkDirRights ; return false end
1477 if not checkDirExistence() ; return false end
1478 createDir()
1479 createTempDir()
1480 setMetadata()
1481 findExtensionOther()
1482 setFileNames()
1483 createFiles()
1484 @status = true
1485 end
1486
1487 def findExtensionOther
1488 if @settings['other']
1489 @settings['othersettings'] =~ /"%o".\S+/ # ruby magic, match %o.+ any characters that are not like spaces
1490 @otherExtension = $&[4..-1]
1491 @settings['othersettings'].gsub!(@otherExtension, '') # remove any references to the ext in the settings
1492 end
1493 end
1494
1495 # create playlist + cuesheet files
1496 def createFiles
1497 ['flac','vorbis','mp3','wav','other'].each do |codec|
1498 if @settings[codec] && @settings['playlist'] && !@settings['image']
1499 createPlaylist(codec)
1500 end
1501 end
1502 end
1503
1504 # check write access of the output dirs
1505 def checkDirRights
1506 @dir.values.each do |directory|
1507 dir = directory
1508 # search for the first existing directory
1509 while not File.directory?(dir) ; dir = File.dirname(dir) end
1510
1511 if not File.writable?(dir)
1512 @status = ["error", _("Can't create output directory!\nYou have no writing acces in dir %s") % [dir]]
1513 return false
1514 end
1515 end
1516 return true
1517 end
1518
1519 # check the existence of the output dir
1520 def checkDirExistence
1521 @dir.values.each do |dir|
1522 puts dir if @settings['debug']
1523 if File.directory?(dir)
1524 @status = ["dir_exists", dir]
1525 return false
1526 end
1527 end
1528 return true
1529 end
1530
1531 # create the output dirs
1532 def createDir
1533 @dir.values.each{|dir| FileUtils.mkdir_p(dir)}
1534 end
1535
1536 # create the temp dir
1537 def createTempDir
1538 if not File.directory?(getTempDir)
1539 FileUtils.mkdir_p(getTempDir)
1540 end
1541 end
1542
1543 # fill the @file variable, so we have for example @file['flac'][1]
1544 def setFileNames
1545 @codecs.each do |codec|
1546 if @settings[codec]
1547 @file[codec] = Hash.new
1548 if @settings['image']
1549 @image[codec] = giveFileName(codec)
1550 else
1551 @settings['cd'].audiotracks.times do |track|
1552 @file[codec][track + 1] = giveFileName(codec, track)
1553 end
1554 end
1555 end
1556 end
1557
1558 #if no hidden track is detected, getStartSector will return false
1559 if @settings['cd'].getStartSector(0)
1560 setHiddenTrack()
1561 end
1562 end
1563
1564 # give the filename for given codec and track
1565 def giveFileName(codec, track=0)
1566 file = @fileName.dup
1567
1568 # the artist should always refer to the artist that is valid for the track
1569 if getVarArtist(track + 1) == '' ; artist = @md.artist ; varArtist = ''
1570 else artist = getVarArtist(track + 1) ; varArtist = @md.artist end
1571
1572 {'%a' => artist, '%b' => @md.album, '%f' => codec, '%g' => @md.genre,
1573 '%y' => @md.year, '%n' => sprintf("%02d", track + 1), '%va' => varArtist,
1574 '%t' => getTrackname(track + 1)}.each do |key, value|
1575 file.gsub!(key, value)
1576 end
1577
1578 # other codec has the extension already in the command
1579 if codec == 'flac' ; file += '.flac'
1580 elsif codec == 'vorbis' ; file += '.ogg'
1581 elsif codec == 'mp3' ; file += '.mp3'
1582 elsif codec == 'wav' ; file += '.wav'
1583 elsif codec == 'other' ; file += @otherExtension
1584 end
1585
1586 filename = fileFilter(file)
1587 puts filename if @settings['debug']
1588 return filename
1589 end
1590
1591 # Fill the metadata, made ready for tagging
1592 def setMetadata
1593 @artist = tagFilter(@md.artist)
1594 @album = tagFilter(@md.album)
1595 @genre = tagFilter(@md.genre)
1596 @year = tagFilter(@md.year)
1597 @settings['cd'].audiotracks.times do |track|
1598 @tracklist[track+1] = tagFilter(@md.tracklist[track])
1599 end
1600 if not @md.varArtists.empty?
1601 @settings['cd'].audiotracks.times do |track|
1602 @varArtists[track+1] = tagFilter(@md.varArtists[track])
1603 end
1604 end
1605 end
1606
1607 # Fill the metadata for the hidden track
1608 def setHiddenTrack
1609 @tracklist[0] = tagFilter(_("Hidden Track").dup)
1610 @varArtists[0] = tagFilter(_("Unknown Artist").dup) if not @md.varArtists.empty?
1611 @codecs.each{|codec| @file[codec][0] = giveFileName(codec, -1) if @settings[codec]}
1612 end
1613
1614 # characters that will be changed for filenames (monkeyproof for FAT32)
1615 def fileFilter(var, isDir=false)
1616 if not isDir
1617 var.gsub!('/', '') #no slashes allowed in filenames
1618 end
1619 var.gsub!(':', '') #no colons allowed in FAT
1620 var.gsub!('*', '') #no asterix allowed in FAT
1621 var.gsub!('?', '') #no question mark allowed in FAT
1622 var.gsub!('<', '') #no smaller than allowed in FAT
1623 var.gsub!('>', '') #no greater than allowed in FAT
1624 var.gsub!('|', '') #no pipe allowed in FAT
1625 var.gsub!('\\', '') #the \\ means a normal \
1626 var.gsub!('"', '')
1627
1628 allFilter(var)
1629
1630 if @settings['noSpaces'] ; var.gsub!(" ", "_") end
1631 if @settings['noCapitals'] ; var.downcase! end
1632 return var.strip
1633 end
1634
1635 #characters that will be changed for tags
1636 def tagFilter(var)
1637 allFilter(var)
1638
1639 #Add a slash before the double quote chars,
1640 #otherwise the shell will complain
1641 var.gsub!('"', '\"')
1642 return var.strip
1643 end
1644
1645 # characters that will be changed for tags and filenames
1646 def allFilter(var)
1647 var.gsub!('`', "'")
1648
1649 # replace any underscores with spaces, some freedb info got
1650 # underscores instead of spaces
1651 if not @settings['noSpaces'] ; var.gsub!('_', ' ') end
1652
1653 if var.respond_to?(:encoding)
1654 # prepare for byte substitutions
1655 enc = var.encoding
1656 #var.force_encoding("ASCII-8BIT")
1657 var.force_encoding(enc)
1658 end
1659
1660 # replace utf-8 single quotes with latin single quote
1661 #var.gsub!(/\342\200\230|\342\200\231/, "'")
1662
1663 # replace utf-8 double quotes with latin double quote
1664 #var.gsub!(/\342\200\234|\342\200\235/, '"')
1665
1666 if var.respond_to?(:encoding)
1667 # restore the old encoding
1668 var.force_encoding(enc)
1669 end
1670 end
1671
1672 # add the first free number as a postfix to the output dir
1673 def postfixDir
1674 postfix = 1
1675 @dir.values.each do |dir|
1676 while File.directory?(dir + "\##{postfix}")
1677 postfix += 1
1678 end
1679 end
1680 @dir.keys.each{|key| @dir[key] = @dir[key] += "\##{postfix}"}
1681 attemptDirCreation()
1682 end
1683
1684 # remove the existing dir, starting with the files in it
1685 def overwriteDir
1686 @dir.values.each{|dir| cleanDir(dir) if File.directory?(dir)}
1687 attemptDirCreation()
1688 end
1689
1690 # clean a directory, starting with the files in it
1691 def cleanDir(dir)
1692 Dir.foreach(dir) do |file|
1693 if File.directory?(file) && file[0..0] != '.' ; cleanDir(File.join(dir, file)) end
1694 filename = File.join(dir, file)
1695 File.delete(filename) if File.file?(filename)
1696 end
1697 Dir.delete(dir)
1698 end
1699
1700 # create Playlist for each codec
1701 def createPlaylist(codec)
1702 @artistFile.force_encoding("UTF-8")
1703 @albumFile.force_encoding("UTF-8")
1704 playlist = File.new(File.join(@dir[codec],
1705 "#{@artistFile} - #{@albumFile} (#{codec}).m3u"), 'w')
1706
1707 @settings['tracksToRip'].each do |track|
1708 playlist.puts @file[codec][track]
1709 end
1710
1711 playlist.close
1712 end
1713
1714 # clean temporary Dir (when finished)
1715 def cleanTempDir
1716 cleanDir(getTempDir()) if File.directory?(getTempDir())
1717 end
1718
1719 # return the first directory (for the summary)
1720 def getDir
1721 return @dir.values[0]
1722 end
1723
1724 # return the full filename of the track (starting with 1) or image
1725 def getFile(track, codec)
1726 if track == "image"
1727 return File.join(@dir[codec], @image[codec])
1728 else
1729 return File.join(@dir[codec].force_encoding("UTF-8"), @file[codec][track].force_encoding("UTF-8"))
1730 end
1731 end
1732
1733 # return the toc file of AdvancedToc class
1734 def getTocFile
1735 return File.join(getTempDir(), "#{@artistFile} - #{@albumFile}.toc")
1736 end
1737
1738 # return the full filename of the log
1739 def getLogFile(codec)
1740 return File.join(@dir[codec], 'ripping.log')
1741 end
1742
1743 # return the full filename of the cuesheet
1744 def getCueFile(codec)
1745 return File.join(@dir[codec], "#{@artistFile} - #{@albumFile} (#{codec}).cue")
1746 end
1747
1748 def getTempFile(track, trial)
1749 if track == "image"
1750 return File.join(getTempDir(), "image_#{trial}.wav")
1751 else
1752 return File.join(getTempDir(), "track#{track}_#{trial}.wav")
1753 end
1754 end
1755
1756 #return the temporary dir
1757 def getTempDir
1758 return File.join(File.dirname(@dir.values[0]), "temp_#{File.basename(@settings['cd'].cdrom)}/")
1759 end
1760
1761 #return the trackname for the metadata
1762 def getTrackname(track)
1763 if @tracklist[track] == nil
1764 return ''
1765 else
1766 return @tracklist[track]
1767 end
1768 end
1769
1770 #return the artist for the metadata
1771 def getVarArtist(track)
1772 if @varArtists[track] == nil
1773 return ''
1774 else
1775 return @varArtists[track]
1776 end
1777 end
1778end
1779
1780require 'fileutils'
1781
1782class SecureRip
1783 attr_writer :cancelled
1784
1785 BYTES_WAV_CONTAINER = 44 # wav container overhead
1786 BYTES_AUDIO_SECTOR = 2352 # size for a audiocd sector as used in cdparanoia
1787 BYTES_SECTOR_GROUP = 1024 * BYTES_AUDIO_SECTOR # compare groups first due performance issue ruby 1.8
1788
1789 def initialize(settings, encoding)
1790 @settings = settings
1791 @encoding = encoding
1792 @cancelled = false
1793 @reqMatchesAll = @settings['req_matches_all'] # Matches needed for all chunks
1794 @reqMatchesErrors = @settings['req_matches_errors'] # Matches needed for chunks that didn't match immediately
1795 @progress = 0.0 #for the progressbar
1796 @sizeExpected = 0
1797 @timeStarted = Time.now # needed for a time break after 30 minutes
1798 ripTracks()
1799 end
1800
1801 def ripTracks
1802 @settings['log'].ripPerc(0.0, "ripper") # Give a hint to the gui that ripping has started
1803
1804 @settings['tracksToRip'].each do |track|
1805 break if @cancelled == true
1806 puts "Ripping track #{track}" if @settings['debug'] && track != 'image'
1807 puts "Ripping image" if @settings['debug'] && track == 'image'
1808 ripTrack(track)
1809 end
1810
1811 eject(@settings['cd'].cdrom) if @settings['eject']
1812 end
1813
1814
1815 # Due to a bug in cdparanoia the -Z setting has to be replaced for last track.
1816 # This is only needed when an offset is set. See issue nr. 13.
1817 def checkParanoiaSettings(track)
1818 if @settings['rippersettings'].include?('-Z') && @settings['offset'] != 0
1819 if track == "image" || track == @settings['cd'].audiotracks
1820 @settings['rippersettings'].gsub!(/-Z\s?/, '')
1821 end
1822 end
1823 end
1824
1825 # rip one output file
1826 def ripTrack(track)
1827 checkParanoiaSettings(track)
1828
1829 #reset next three variables for each track
1830 @errors = Hash.new()
1831 @filesizes = Array.new
1832 @trial = 0
1833
1834 # first check if there's enough size available in the output dir
1835 if sizeTest(track)
1836 if main(track)
1837 deEmphasize(track)
1838 @encoding.addTrack(track)
1839 else
1840 return false
1841 end #ready to encode
1842 end
1843 end
1844
1845 # check if the track needs to be corrected
1846 # the de-emphasized file needs another name
1847 # when sox is finished move it back to the original name
1848 def deEmphasize(track)
1849 if @settings['create_cue'] && @settings['preEmphasis'] == "sox" &&
1850 @settings['cd'].toc.hasPreEmph(track) && installed('sox')
1851 `sox #{@settings['Out'].getTempFile(track, 1)} #{@settings['Out'].getTempFile(track, 2)}`
1852 if $?.success?
1853 FileUtils.mv(@settings['Out'].getTempFile(track, 2), @settings['Out'].getTempFile(track, 1))
1854 else
1855 puts "sox failed somehow."
1856 end
1857 end
1858 end
1859
1860 def sizeTest(track)
1861 puts "Expected filesize for #{if track == "image" ; track else "track #{track}" end}\
1862 is #{@settings['cd'].getFileSize(track)} bytes." if @settings['debug']
1863
1864 if installed('df')
1865 freeDiskSpace = `LANG=C df \"#{@settings['Out'].getDir()}\"`.split()[10].to_i
1866 puts "Free disk space is #{freeDiskSpace} MB" if @settings['debug']
1867 if @settings['cd'].getFileSize(track) > freeDiskSpace*1000
1868 @settings['log'].add(_("Not enough disk space left! Rip aborted"))
1869 return false
1870 end
1871 end
1872 return true
1873 end
1874
1875 def main(track)
1876 @reqMatchesAll.times{if not doNewTrial(track) ; return false end} # The amount of matches all sectors should match
1877 analyzeFiles(track) #If there are differences, save them in the @errors hash
1878
1879 while @errors.size > 0
1880 if @trial > @settings['max_tries'] && @settings['max_tries'] != 0 # We would like to respect our users settings, wouldn't we?
1881 @settings['log'].add(_("Maximum tries reached. %s chunk(s) didn't match the required %s times\n") % [@errors.length, @reqMatchesErrors])
1882 @settings['log'].add(_("Will continue with the file we've got so far\n"))
1883 @settings['log'].mismatch(track, 0, @errors.keys, @settings['cd'].getFileSize(track), @settings['cd'].getLengthSector(track)) # zero means it is never solved.
1884 break # break out loop and continue using trial1
1885 end
1886
1887 doNewTrial(track)
1888 break if @cancelled == true
1889
1890 if @trial > @reqMatchesErrors # If the reqMatches errors is equal of higher to @trial, no match would ever be found, so skip
1891 correctErrorPos(track)
1892 else
1893 readErrorPos(track)
1894 end
1895 end
1896
1897 getDigest(track) # Get a MD5-digest for the logfile
1898 @progress += @settings['percentages'][track]
1899 @settings['log'].ripPerc(@progress)
1900 return true
1901 end
1902
1903 def doNewTrial(track)
1904 fileOk = false
1905
1906 while (!@cancelled && !fileOk)
1907 @trial += 1
1908 rip(track)
1909 if fileCreated(track) && testFileSize(track)
1910 fileOk = true
1911 end
1912 end
1913
1914 # when cancelled fileOk will still be false
1915 return fileOk
1916 end
1917
1918 def fileCreated(track) #check if cdparanoia outputs wav files (passing bad parameters?)
1919 if not File.exist?(@settings['Out'].getTempFile(track, @trial))
1920 @settings['instance'].update("error", _("Cdparanoia doesn't output wav files.\nCheck your settings please."))
1921 return false
1922 end
1923 return true
1924 end
1925
1926 def testFileSize(track) #check if wavfile is of correct size
1927 sizeDiff = @settings['cd'].getFileSize(track) - File.size(@settings['Out'].getTempFile(track, @trial))
1928
1929 # at the end the disc may differ 1 sector on some drives (2352 bytes)
1930 if sizeDiff == 0
1931 # expected size matches exactly
1932 elsif sizeDiff < 0
1933 puts "More sectors ripped than expected: #{sizeDiff / 2352} sector(s)" if @settings['debug']
1934 elsif @settings['offset'] > 0 && (track == "image" || track == @settings['cd'].audiotracks)
1935 @settings['log'].add(_("The ripped file misses %s sectors.\n") % [sizeDiff / 2352.0])
1936 @settings['log'].add(_("This is known behaviour for some drives when using an offset.\n"))
1937 @settings['log'].add(_("Notice that each sector is 1/75 second.\n"))
1938 elsif @cancelled == false
1939 if @settings['debug']
1940 puts "Some sectors are missing for track #{track} : #{sizeDiff} sector(s)"
1941 puts "Filesize should be : #{@settings['cd'].getFileSize(track)}"
1942 end
1943
1944 #someone might get out of free diskspace meanwhile
1945 @cancelled = true if not sizeTest(track)
1946
1947 File.delete(@settings['Out'].getTempFile(track, @trial)) # Delete file with wrong filesize
1948 @trial -= 1 # reset the counter because the filesize is not right
1949 @settings['log'].add(_("Filesize is not correct! Trying another time\n"))
1950 return false
1951 end
1952 return true
1953 end
1954
1955 # Start and close the first file comparisons
1956 def analyzeFiles(track)
1957 start = Time.now()
1958 @settings['log'].add(_("Analyzing files for mismatching chunks"))
1959 compareSectors(track)
1960 @settings['log'].add(_(" (%s second(s))\n") %[(Time.now - start).to_i])
1961
1962 # Remove the files now we analyzed them. Differences are saved in memory.
1963 (@reqMatchesAll - 1).times{|time| File.delete(@settings['Out'].getTempFile(track, time + 2))}
1964
1965 if @errors.size == 0
1966 @settings['log'].add(_("Every chunk matched %s times :)\n") % [@reqMatchesAll])
1967 else
1968 @settings['log'].mismatch(track, @trial, @errors.keys, @settings['cd'].getFileSize(track), @settings['cd'].getLengthSector(track)) # report for later position analysis
1969 @settings['log'].add(_("%s chunk(s) didn't match %s times.\n") % [@errors.length, @reqMatchesAll])
1970 end
1971 end
1972
1973 # Compare a range of sectors within two files
1974 def compareSectorRange(files, fileIndexA, fileIndexB, sectorOffset)
1975
1976 # First do one large block compare, and bail out if the same.
1977 # A lot faster than sector by sector comparison on some Ruby implementations
1978 # (http://code.google.com/p/rubyripper/issues/detail?id=348).
1979 fa = files[fileIndexA]
1980 fb = files[fileIndexB]
1981 fa.sysseek(BYTES_WAV_CONTAINER + sectorOffset, IO::SEEK_SET)
1982 fb.sysseek(BYTES_WAV_CONTAINER + sectorOffset, IO::SEEK_SET)
1983 if fa.sysread(BYTES_SECTOR_GROUP) == fb.sysread(BYTES_SECTOR_GROUP)
1984 return
1985 end
1986
1987 # There was a difference, so drill down and find the individual sectors
1988 pos = sectorOffset
1989 endPos = pos + BYTES_SECTOR_GROUP
1990 begin
1991 while pos < endPos && pos < @endSectorOffset
1992 # If we haven't already recorded an error for this sector
1993 if !@errors.key?(pos)
1994 # Is there a mismatch
1995 fa.sysseek(BYTES_WAV_CONTAINER + pos, IO::SEEK_SET)
1996 fb.sysseek(BYTES_WAV_CONTAINER + pos, IO::SEEK_SET)
1997 if fa.sysread(BYTES_AUDIO_SECTOR) != fb.sysread(BYTES_AUDIO_SECTOR)
1998 # Store a copy of the sector from each file in the error map
1999 files.each{|file| file.sysseek(BYTES_WAV_CONTAINER + pos, IO::SEEK_SET)}
2000 @errors[pos] = Array.new
2001 files.each{|file| @errors[pos] << file.sysread(BYTES_AUDIO_SECTOR)}
2002 end
2003 end
2004 pos += BYTES_AUDIO_SECTOR
2005 end
2006 rescue EOFError
2007 puts "An unexpected end-of-file error occured for position #{pos}."
2008 puts "Final sector should start at #{@endSectorOffset}."
2009 puts "This sector is now ignored for comparison!!"
2010 end
2011 end
2012
2013 # Compare each temp file, recording all sectors which don't match in @errors
2014 def compareSectors(track)
2015 files = Array.new
2016 @reqMatchesAll.times do |time|
2017 files << File.new(@settings['Out'].getTempFile(track, time + 1), 'r')
2018 end
2019
2020 @endSectorOffset = @settings['cd'].getFileSize(track) - BYTES_WAV_CONTAINER
2021
2022 (@reqMatchesAll - 1).times do |time|
2023 sectorOffset = 0
2024 while sectorOffset < @endSectorOffset
2025 compareSectorRange(files, 0, time + 1, sectorOffset)
2026 sectorOffset += BYTES_SECTOR_GROUP
2027 end
2028 end
2029
2030 files.each{|file| file.close}
2031 end
2032
2033 # When required matches for mismatched sectors are bigger than there are
2034 # trials to be tested, readErrorPos() just reads the mismatched sectors
2035 # without analysing them.
2036 # Wav-containter overhead = 44 bytes.
2037 # Audio-cd sector = 2352 bytes.
2038
2039 def readErrorPos(track)
2040 file = File.new(@settings['Out'].getTempFile(track, @trial), 'r')
2041 @errors.keys.sort.each do |start_chunk|
2042 file.sysseek(start_chunk + BYTES_WAV_CONTAINER, IO::SEEK_SET)
2043 @errors[start_chunk] << file.sysread(BYTES_AUDIO_SECTOR)
2044 end
2045 file.close
2046
2047 # Remove the file now we read it. Differences are saved in memory.
2048 File.delete(@settings['Out'].getTempFile(track, @trial))
2049
2050 # Give an update for the trials for later analysis
2051 @settings['log'].mismatch(track, @trial, @errors.keys, @settings['cd'].getFileSize(track), @settings['cd'].getLengthSector(track))
2052 end
2053
2054 # Let the errors 'wave' out. For each sector that isn't unique across
2055 # different trials, try to find at least @reqMatchesErrors matches. If
2056 # indeed this amount of matches is found, correct the sector in the
2057 # reference file (trial 1).
2058
2059 def correctErrorPos(track)
2060 file1 = File.new(@settings['Out'].getTempFile(track, 1), 'r+')
2061 file2 = File.new(@settings['Out'].getTempFile(track, @trial), 'r')
2062
2063 # Sort the hash keys to prevent jumping forward and backwards in the file
2064 @errors.keys.sort.each do |start_chunk|
2065 file2.sysseek(start_chunk + BYTES_WAV_CONTAINER, IO::SEEK_SET)
2066 @errors[start_chunk] << temp = file2.sysread(BYTES_AUDIO_SECTOR)
2067
2068 # now sort the array and see if the new read value has enough matches
2069 # right index minus left index of the read value is amount of matches
2070 @errors[start_chunk].sort!
2071 if (@errors[start_chunk].rindex(temp) - @errors[start_chunk].index(temp)) == (@reqMatchesErrors - 1)
2072 file1.sysseek(start_chunk + BYTES_WAV_CONTAINER, IO::SEEK_SET)
2073 file1.syswrite(temp)
2074 @errors.delete(start_chunk)
2075 end
2076 end
2077
2078 file1.close
2079 file2.close
2080
2081 # Remove the file now we read it. Differences are saved in memory.
2082 File.delete(@settings['Out'].getTempFile(track, @trial))
2083
2084 #give an update of the amount of errors and trials
2085 if @errors.size == 0
2086 @settings['log'].add(_("Error(s) succesfully corrected, %s matches found for each chunk :)\n") % [@reqMatchesErrors])
2087 else
2088 @settings['log'].mismatch(track, @trial, @errors.keys, @settings['cd'].getFileSize(track), @settings['cd'].getLengthSector(track)) # report for later position analysis
2089 @settings['log'].add(_("%s chunk(s) didn't match %s times.\n") % [@errors.length, @reqMatchesErrors])
2090 end
2091 end
2092
2093 # add a timeout if a disc takes longer than 30 minutes to rip (this might save the hardware and the disc)
2094 def cooldownNeeded
2095 puts "Minutes ripping is #{(Time.now - @timeStarted) / 60}." if @settings['debug']
2096
2097 if (((Time.now - @timeStarted) / 60) > 30 && @settings['maxThreads'] != 0)
2098 @settings['log'].add(_("The drive is spinning for more than 30 minutes.\n"))
2099 @settings['log'].add(_("Taking a timeout of 2 minutes to protect the hardware.\n"))
2100 sleep(120)
2101 @timeStarted = Time.now # reset time
2102 end
2103 end
2104
2105 def rip(track) # set cdparanoia command + parameters
2106 cooldownNeeded()
2107
2108 timeStarted = Time.now
2109
2110 if track == "image"
2111 @settings['log'].add(_("Starting to rip CD image, trial \#%s") % [@trial])
2112 else
2113 @settings['log'].add(_("Starting to rip track %s, trial \#%s") % [track, @trial])
2114 end
2115
2116 command = "cdparanoia"
2117
2118 if @settings['rippersettings'].size != 0
2119 command += " #{@settings['rippersettings']}"
2120 end
2121
2122 command += " [.#{@settings['cd'].getStartSector(track)}]-"
2123
2124 # for the last track tell cdparanoia to rip till end to prevent problems on some drives
2125 if track != "image" && track != @settings['cd'].audiotracks
2126 command += "[.#{@settings['cd'].getLengthSector(track) - 1}]"
2127 end
2128
2129 # the ported cdparanoia for MacOS misses the -d option, default drive will be used.
2130 if @settings['cd'].multipleDriveSupport ; command += " -d #{@settings['cdrom']}" end
2131
2132 command += " -O #{@settings['offset']}"
2133 command += " \"#{@settings['Out'].getTempFile(track, @trial)}\""
2134 unless @settings['verbose'] ; command += " 2>&1" end # hide the output of cdparanoia output
2135 puts command if @settings['debug']
2136 `#{command}` if @cancelled == false #Launch the cdparanoia command
2137 @settings['log'].add(" (#{(Time.now - timeStarted).to_i} #{_("seconds")})\n")
2138 end
2139
2140 def getDigest(track)
2141 digest = Digest::MD5.new()
2142 file = File.open(@settings['Out'].getTempFile(track, 1), 'r')
2143 index = 0
2144 while (index < @settings['cd'].getFileSize(track))
2145 digest << file.sysread(100000)
2146 index += 100000
2147 end
2148 file.close()
2149 @settings['log'].add(_("MD5 sum: %s\n\n") % [digest.hexdigest])
2150 end
2151end
2152
2153class Encode
2154 attr_writer :cancelled
2155
2156 require 'thread'
2157
2158 def initialize(settings)
2159 @settings = settings
2160 @cancelled = false
2161 @progress = 0.0
2162 @threads = []
2163 @queue = SizedQueue.new(@settings['maxThreads']) if @settings['maxThreads'] != 0
2164 @lock = Monitor.new
2165 @out = @settings['Out'] # create a shortcut
2166
2167 # Set the charset environment variable to UTF-8. Oggenc needs this.
2168 # Perhaps others need it as well.
2169 ENV['CHARSET'] = "UTF-8"
2170
2171 @codecs = 0 # number of codecs
2172 ['flac','vorbis','mp3','wav','other'].each do |codec|
2173 @codecs += 1 if @settings[codec]
2174 end
2175
2176 # all encoding tasks are saved here, to determine when to delete a wav
2177 @tasks = Hash.new
2178 @settings['tracksToRip'].each{|track| @tasks[track] = @codecs}
2179 end
2180
2181 # is called when a track is ripped succesfully
2182 def addTrack(track)
2183 if normalize(track)
2184 startEncoding(track)
2185 end
2186 end
2187
2188 # encode track when normalize is finished
2189 def startEncoding(track)
2190 # mark the progress bar as being started
2191 @settings['log'].encPerc(0.0) if track == @settings['tracksToRip'][0]
2192 ['flac', 'vorbis', 'mp3', 'wav', 'other'].each do |codec|
2193 if @settings[codec] && @cancelled == false
2194 if @settings['maxThreads'] == 0
2195 encodeTrack(track,codec)
2196 else
2197 puts "Adding track #{track} (#{codec}) to the queue.." if @settings['debug']
2198 @queue << 1 # add a value to the queue, if full wait here.
2199 @threads << Thread.new do
2200 encodeTrack(track,codec)
2201 puts "Removing track #{track} (#{codec}) from the queue.." if @settings['debug']
2202 @queue.shift() # move up in the queue to the first waiter
2203 end
2204 end
2205 end
2206 end
2207
2208 #give the signal we're finished
2209 if track == @settings['tracksToRip'][-1] && @cancelled == false
2210 @threads.each{|thread| thread.join()}
2211 finished()
2212 end
2213 end
2214
2215 # respect the normalize setting
2216 def normalize(track)
2217 continue = true
2218 if @settings['normalize'] != 'normalize'
2219 elsif !installed('normalize')
2220 puts "WARNING: normalize is not installed on your system!"
2221 elsif @settings['gain'] == 'album' && @settings['tracksToRip'][-1] == track
2222 command = "normalize -b \"#{File.join(@out.getTempDir(),'*.wav')}\""
2223 `#{command}`
2224 # now the wavs are altered, the encoding can start
2225 @settings['tracksToRip'].each{|track| startEncoding(track)}
2226 continue = false
2227 elsif @settings['gain'] == 'track'
2228 command = "normalize \"#{@out.getTempFile(track, 1)}\""
2229 `#{command}`
2230 end
2231 return continue
2232 end
2233
2234 # call the specific codec function for the track
2235 def encodeTrack(track, codec)
2236 if codec == 'flac' ; doFlac(track)
2237 elsif codec == 'vorbis' ; doVorbis(track)
2238 elsif codec == 'mp3' ; doMp3(track)
2239 elsif codec == 'wav' ; doWav(track)
2240 elsif codec == 'other' && @settings['othersettings'] != nil ; doOther(track)
2241 end
2242
2243 @lock.synchronize do
2244 File.delete(@out.getTempFile(track, 1)) if (@tasks[track] -= 1) == 0
2245 updateProgress(@settings['percentages'][track] / @codecs)
2246 end
2247 end
2248
2249 # update the gui
2250 def updateProgress(progress)
2251 @progress += progress
2252 @settings['log'].encPerc(@progress)
2253 end
2254
2255
2256 def finished
2257 puts "Inside the finished function" if @settings['debug']
2258 @progress = 1.0 ; @settings['log'].encPerc(@progress)
2259 @settings['log'].summary(@settings['req_matches_all'], @settings['req_matches_errors'], @settings['max_tries'])
2260 if @settings['no_log'] ; @settings['log'].delLog end #Delete the logfile if no correction was needed if no_log is true
2261 @out.cleanTempDir()
2262 if (@settings['log'].rippingErrors || @settings['log'].encodingErrors)
2263 @settings['instance'].update("finished", false)
2264 else
2265 @settings['instance'].update("finished", true)
2266 end
2267 end
2268
2269 def replaygain(filename, codec, track)
2270 if @settings['normalize'] == "replaygain"
2271 if @settings['gain'] == "album" && @settings['tracksToRip'][-1] == track || @settings['gain']=="track"
2272 if codec == 'flac'
2273 if not installed('metaflac') ; puts "WARNING: Metaflac is not installed. Cannot replaygain files." ; return false end
2274 command = "metaflac --add-replay-gain \"#{if @settings['gain'] =="track" ; filename else File.dirname(filename) + "\"/*.flac" end}"
2275 `#{command}`
2276 elsif codec == 'vorbis'
2277 if not installed('vorbisgain') ; puts "WARNING: Vorbisgain is not installed. Cannot replaygain files." ; return false end
2278 command = "vorbisgain #{if @settings['gain'] =="track" ; "\"" + filename + "\"" else "-a \"" + File.dirname(filename) + "\"/*.ogg" end}"
2279 `#{command}`
2280 elsif codec == 'mp3'
2281 if not installed('mp3gain') ; puts "WARNING: Mp3gain is not installed. Cannot replaygain files." ; return false end
2282 if @settings['gainTagsOnly']
2283 command = "mp3gain -c #{if @settings['gain'] =="track" ; "\"" + filename + "\"" else "\"" + File.dirname(filename) + "\"/*.mp3" end}"
2284 else
2285 command = "mp3gain -c #{if @settings['gain'] =="track" ; "-r \"" + filename + "\"" else "-a \"" + File.dirname(filename) + "\"/*.mp3" end}"
2286 end
2287 `#{command}`
2288 elsif codec == 'wav'
2289 if @settings['gainTagsOnly']
2290 puts "No replay gain tags possible for wav codec."
2291 else
2292 if not installed('wavegain') ; puts "WARNING: Wavegain is not installed. Cannot replaygain files." ; return false end
2293 command = "wavegain #{if @settings['gain'] =="track" ; "\"" + filename +"\"" else "-a \"" + File.dirname(filename) + "\"/*.wav" end}"
2294 `#{command}`
2295 end
2296 end
2297 end
2298 end
2299 end
2300
2301 def doFlac(track)
2302 filename = @out.getFile(track, 'flac')
2303 if !@settings['flacsettings'] ; @settings['flacsettings'] = '--best' end
2304 flac(filename, track)
2305 replaygain(filename, 'flac', track)
2306 end
2307
2308 def doVorbis(track)
2309 filename = @out.getFile(track, 'vorbis')
2310 if !@settings['vorbissettings'] ; @settings['vorbissettings'] = '-q 6' end
2311 vorbis(filename, track)
2312 replaygain(filename, 'vorbis', track)
2313 end
2314
2315 def doMp3(track)
2316 @possible_lame_tags = ['A CAPPELLA', 'ACID', 'ACID JAZZ', 'ACID PUNK', 'ACOUSTIC', 'ALTERNATIVE', 'ALT. ROCK', 'AMBIENT', 'ANIME', 'AVANTGARDE', \
2317'BALLAD', 'BASS', 'BEAT', 'BEBOB', 'BIG BAND', 'BLACK METAL', 'BLUEGRASS', 'BLUES', 'BOOTY BASS', 'BRITPOP', 'CABARET', 'CELTIC', 'CHAMBER MUSIC', 'CHANSON', \
2318'CHORUS', 'CHRISTIAN GANGSTA RAP', 'CHRISTIAN RAP', 'CHRISTIAN ROCK', 'CLASSICAL', 'CLASSIC ROCK', 'CLUB', 'CLUB-HOUSE', 'COMEDY', 'CONTEMPORARY CHRISTIAN', \
2319'COUNTRY', 'CROSSOVER', 'CULT', 'DANCE', 'DANCE HALL', 'DARKWAVE', 'DEATH METAL', 'DISCO', 'DREAM', 'DRUM & BASS', 'DRUM SOLO', 'DUET', 'EASY LISTENING', \
2320'ELECTRONIC', 'ETHNIC', 'EURODANCE', 'EURO-HOUSE', 'EURO-TECHNO', 'FAST-FUSION', 'FOLK', 'FOLKLORE', 'FOLK/ROCK', 'FREESTYLE', 'FUNK', 'FUSION', 'GAME', \
2321'GANGSTA RAP', 'GOA', 'GOSPEL', 'GOTHIC', 'GOTHIC ROCK', 'GRUNGE', 'HARDCORE', 'HARD ROCK', 'HEAVY METAL', 'HIP-HOP', 'HOUSE', 'HUMOUR', 'INDIE', 'INDUSTRIAL', \
2322'INSTRUMENTAL', 'INSTRUMENTAL POP', 'INSTRUMENTAL ROCK', 'JAZZ', 'JAZZ+FUNK', 'JPOP', 'JUNGLE', 'LATIN', 'LO-FI', 'MEDITATIVE', 'MERENGUE', 'METAL', 'MUSICAL', \
2323'NATIONAL FOLK', 'NATIVE AMERICAN', 'NEGERPUNK', 'NEW AGE', 'NEW WAVE', 'NOISE', 'OLDIES', 'OPERA', 'OTHER', 'POLKA', 'POLSK PUNK', 'POP', 'POP-FOLK', 'POP/FUNK', \
2324'PORN GROOVE', 'POWER BALLAD', 'PRANKS', 'PRIMUS', 'PROGRESSIVE ROCK', 'PSYCHEDELIC', 'PSYCHEDELIC ROCK', 'PUNK', 'PUNK ROCK', 'RAP', 'RAVE', 'R&B', 'REGGAE', \
2325'RETRO', 'REVIVAL', 'RHYTHMIC SOUL', 'ROCK', 'ROCK & ROLL', 'SALSA', 'SAMBA', 'SATIRE', 'SHOWTUNES', 'SKA', 'SLOW JAM', 'SLOW ROCK', 'SONATA', 'SOUL', 'SOUND CLIP', \
2326'SOUNDTRACK', 'SOUTHERN ROCK', 'SPACE', 'SPEECH', 'SWING', 'SYMPHONIC ROCK', 'SYMPHONY', 'SYNTHPOP', 'TANGO', 'TECHNO', 'TECHNO-INDUSTRIAL', 'TERROR', 'THRASH METAL', \
2327'TOP 40', 'TRAILER', 'TRANCE', 'TRIBAL', 'TRIP-HOP', 'VOCAL']
2328 filename = @out.getFile(track, 'mp3')
2329 if !@settings['mp3settings'] ; @settings['mp3settings'] = "--preset fast standard" end
2330
2331 # lame versions before 3.98 didn't support other genre tags than the
2332 # ones defined above, so change it to 'other' to prevent crashes
2333 lameVersion = `lame --version`[20,4].split('.') # for example [3, 98]
2334 if (lameVersion[0] == '3' && lameVersion[1].to_i < 98 &&
2335 !@possible_lame_tags.include?(@out.genre.upcase))
2336 genre = 'other'
2337 else
2338 genre = @out.genre
2339 end
2340
2341 mp3(filename, genre, track)
2342 replaygain(filename, 'mp3', track)
2343 end
2344
2345 def doWav(track)
2346 filename = @out.getFile(track, 'wav')
2347 wav(filename, track)
2348 replaygain(filename, 'wav', track)
2349 end
2350
2351 def doOther(track)
2352 filename = @out.getFile(track, 'other')
2353 command = @settings['othersettings'].dup
2354
2355 command.force_encoding("UTF-8") if command.respond_to?("force_encoding")
2356 command.gsub!('%n', sprintf("%02d", track)) if track != "image"
2357 command.gsub!('%f', 'other')
2358
2359 if @out.getVarArtist(track) != ''
2360 command.gsub!('%a', @out.getVarArtist(track))
2361 command.gsub!('%va', @out.artist)
2362 else
2363 command.gsub!('%a', @out.artist)
2364 end
2365
2366 command.gsub!('%b', @out.album)
2367 command.gsub!('%g', @out.genre)
2368 command.gsub!('%y', @out.year)
2369 command.gsub!('%t', @out.getTrackname(track))
2370 command.gsub!('%i', @out.getTempFile(track, 1))
2371 command.gsub!('%o', @out.getFile(track, 'other'))
2372 checkCommand(command, track, 'other')
2373 end
2374
2375 def flac(filename, track)
2376 tags = String.new
2377 tags.force_encoding("UTF-8") if tags.respond_to?("force_encoding")
2378 tags += "--tag ALBUM=\"#{@out.album}\" "
2379 tags += "--tag DATE=\"#{@out.year}\" "
2380 tags += "--tag GENRE=\"#{@out.genre}\" "
2381 tags += "--tag DISCID=\"#{@settings['cd'].discId}\" "
2382 tags += "--tag DISCNUMBER=\"#{@settings['cd'].md.discNumber}\" " if @settings['cd'].md.discNumber
2383
2384 # Handle tags for single file images differently
2385 if @settings['image']
2386 tags += "--tag ARTIST=\"#{@out.artist}\" " #artist is always artist
2387 if @settings['create_cue'] # embed the cuesheet
2388 tags += "--cuesheet=\"#{@out.getCueFile('flac')}\" "
2389 end
2390 else # Handle tags for var artist discs differently
2391 if @out.getVarArtist(track) != ''
2392 tags += "--tag ARTIST=\"#{@out.getVarArtist(track)}\" "
2393 tags += "--tag \"ALBUM ARTIST\"=\"#{@out.artist}\" "
2394 else
2395 tags += "--tag ARTIST=\"#{@out.artist}\" "
2396 end
2397 tags += "--tag TITLE=\"#{@out.getTrackname(track)}\" "
2398 tags += "--tag TRACKNUMBER=#{track} "
2399 tags += "--tag TRACKTOTAL=#{@settings['cd'].audiotracks} "
2400 end
2401
2402 command = String.new
2403 command.force_encoding("UTF-8") if command.respond_to?("force_encoding")
2404 command +="flac #{@settings['flacsettings'].force_encoding("UTF-8")} -o \"#{filename.force_encoding("UTF-8")}\" #{tags.force_encoding("UTF-8")}\
2405\"#{@out.getTempFile(track, 1)}\""
2406 command += " 2>&1" unless @settings['verbose']
2407
2408 checkCommand(command, track, 'flac')
2409 end
2410
2411 def vorbis(filename, track)
2412 tags = String.new
2413 tags.force_encoding("UTF-8") if tags.respond_to?("force_encoding")
2414 tags += "-c ALBUM=\"#{@out.album}\" "
2415 tags += "-c DATE=\"#{@out.year}\" "
2416 tags += "-c GENRE=\"#{@out.genre}\" "
2417 tags += "-c DISCID=\"#{@settings['cd'].discId}\" "
2418 tags += "-c DISCNUMBER=\"#{@settings['cd'].md.discNumber}\" " if @settings['cd'].md.discNumber
2419
2420 # Handle tags for single file images differently
2421 if @settings['image']
2422 tags += "-c ARTIST=\"#{@out.artist}\" "
2423 else # Handle tags for var artist discs differently
2424 if @out.getVarArtist(track) != ''
2425 tags += "-c ARTIST=\"#{@out.getVarArtist(track)}\" "
2426 tags += "-c \"ALBUM ARTIST\"=\"#{@out.artist}\" "
2427 else
2428 tags += "-c ARTIST=\"#{@out.artist}\" "
2429 end
2430 tags += "-c TITLE=\"#{@out.getTrackname(track)}\" "
2431 tags += "-c TRACKNUMBER=#{track} "
2432 tags += "-c TRACKTOTAL=#{@settings['cd'].audiotracks}"
2433 end
2434
2435 command = String.new
2436 command.force_encoding("UTF-8") if command.respond_to?("force_encoding")
2437 command += "oggenc -o \"#{filename}\" #{@settings['vorbissettings']} \
2438#{tags} \"#{@out.getTempFile(track, 1)}\""
2439 command += " 2>&1" unless @settings['verbose']
2440
2441 checkCommand(command, track, 'vorbis')
2442 end
2443
2444 def mp3(filename, genre, track)
2445 tags = String.new
2446 tags.force_encoding("UTF-8") if tags.respond_to?("force_encoding")
2447 tags += "--tl \"#{@out.album}\" "
2448 tags += "--ty \"#{@out.year}\" "
2449 tags += "--tg \"#{@out.genre}\" "
2450 tags += "--tv TXXX=DISCID=\"#{@settings['cd'].discId}\" "
2451 tags += "--tv TPOS=\"#{@settings['cd'].md.discNumber}\" " if @settings['cd'].md.discNumber
2452
2453 # Handle tags for single file images differently
2454 if @settings['image']
2455 tags += "--ta \"#{@out.artist}\" "
2456 else # Handle tags for var artist discs differently
2457 if @out.getVarArtist(track) != ''
2458 tags += "--ta \"#{@out.getVarArtist(track)}\" "
2459 tags += "--tv \"ALBUM ARTIST\"=\"#{@out.artist}\" "
2460 else
2461 tags += "--ta \"#{@out.artist}\" "
2462 end
2463 tags += "--tt \"#{@out.getTrackname(track)}\" "
2464 tags += "--tn #{track}/#{@settings['cd'].audiotracks} "
2465 end
2466
2467 # set UTF-8 tags (not the filename) to latin because of a lame bug.
2468 begin
2469 require 'iconv'
2470 tags = Iconv.conv("ISO-8859-1", "UTF-8", tags)
2471 rescue
2472 puts "couldn't convert to ISO-8859-1 succesfully"
2473 end
2474
2475 # combining two encoding sets in binary mode, only needed for ruby >=1.9
2476 command = String.new
2477 inputWavFile = @out.getTempFile(track, 1)
2478 if command.respond_to?("force_encoding")
2479 command.force_encoding("ASCII-8BIT")
2480 tags.force_encoding("ASCII-8BIT")
2481 inputWavFile.force_encoding("ASCII-8BIT")
2482 filename.force_encoding("ASCII-8BIT")
2483 end
2484
2485 command += "lame #{@settings['mp3settings']} #{tags}\"\
2486#{inputWavFile}\" \"#{filename}\""
2487 command += " 2>&1" unless @settings['verbose']
2488
2489 checkCommand(command, track, 'mp3')
2490 end
2491
2492 def wav(filename, track)
2493 begin
2494 FileUtils.cp(@out.getTempFile(track, 1), filename)
2495 rescue
2496 puts "Warning: wav file #{@out.getTempFile(track,1)} not found!"
2497 puts "If this is not the case, you might have a shortage of disk space.."
2498 end
2499 end
2500
2501 def checkCommand(command, track, codec)
2502 puts "command = #{command}" if @settings['debug']
2503
2504 exec = IO.popen("nice -n 6 #{command}") #execute command
2505 exec.readlines() #get all the output
2506
2507 if Process.waitpid2(exec.pid)[1].exitstatus != 0
2508 @settings['log'].add(_("WARNING: Encoding to %s exited with an error with track %s!\n") % [codec, track])
2509 @settings['log'].encodingErrors = true
2510 end
2511 end
2512end
2513
2514class Rubyripper
2515attr_reader :outputDir
2516
2517 def initialize(settings, gui)
2518 @settings = settings.dup
2519 @directory = false
2520 @settings['log'] = false
2521 @settings['instance'] = gui
2522 @error = false
2523 @encoding = nil
2524 @ripping = nil
2525 end
2526
2527 def settingsOk
2528 if not checkConfig() ; return @error end
2529 if not testDeps() ; return @error end
2530 @settings['cd'].md.saveChanges()
2531 @settings['Out'] = Output.new(@settings)
2532 return @settings['Out'].status
2533 end
2534
2535 def startRip
2536 @settings['log'] = Gui_support.new(@settings)
2537 @outputDir = @settings['Out'].getDir()
2538 updateGui() # Give some info about the cdrom-player, the codecs, the ripper, cddb_info
2539
2540 waitForToc()
2541
2542 @settings['log'].add(_("\nSTATUS\n\n"))
2543
2544 computePercentage() # Do some pre-work to get the progress updater working later on
2545 require 'digest/md5' # Needed for secure class, only have to load them ones here.
2546 @encoding = Encode.new(@settings) #Create an instance for encoding
2547 @ripping = SecureRip.new(@settings, @encoding) #create an instance for ripping
2548 end
2549
2550 # the user wants to abort the ripping
2551 def cancelRip
2552 puts "User aborted current rip"
2553 `killall cdrdao 2>&1`
2554 @encoding.cancelled = true if @encoding != nil
2555 @encoding = nil
2556 @ripping.cancelled = true if @ripping != nil
2557 @ripping = nil
2558 `killall cdparanoia 2>&1` # kill any rip that is already started
2559 end
2560
2561 # wait for the Advanced Toc class to finish
2562 # cdrdao takes a while to finish reading the disc
2563 def waitForToc
2564 if @settings['create_cue'] && installed('cdrdao')
2565 @settings['log'].add(_("\nADVANCED TOC ANALYSIS (with cdrdao)\n"))
2566 @settings['log'].add(_("...please be patient, this may take a while\n\n"))
2567
2568 @settings['cd'].updateSettings(@settings) # update the rip settings
2569
2570 @settings['cd'].toc.log.each do |message|
2571 @settings['log'].add(message)
2572 end
2573 end
2574 end
2575
2576 # check the configuration of the user.
2577 # 1) does the ripping drive exists
2578 # 2) are there tracks selected to rip
2579 # 3) is the current disc the same as loaded in memory
2580 # 4) is at least one codec is selected
2581 # 5) are the otherSettings correct
2582 # 6) is req_matches_all <= req_matches_errors
2583
2584 def checkConfig
2585 unless File.symlink?(@settings['cdrom']) || File.blockdev?(@settings['cdrom'])
2586 @error = ["error", _("The device %s doesn't exist on your system!") % [@settings['cdrom']]]
2587 return false
2588 end
2589
2590 if @settings['tracksToRip'].size == 0
2591 @error = ["error", _("Please select at least one track.")]
2592 return false
2593 end
2594
2595 if (!@settings['cd'].tocStarted || @settings['cd'].tocFinished)
2596 temp = Disc.new(@settings, @settings['instance'], '', true)
2597 if @settings['cd'].freedbString != temp.freedbString || @settings['cd'].playtime != temp.playtime
2598 @error = ["error", _("The Gui doesn't match inserted cd. Please press Scan Drive first.")]
2599 return false
2600 end
2601 end
2602
2603 unless @settings['flac'] || @settings['vorbis'] || @settings['mp3'] || @settings['wav'] || @settings['other']
2604 @error = ["error", _("No codecs are selected!")]
2605 return false
2606 end
2607
2608 # filter out encoding flags that do non-encoding tasks
2609 @settings['flacsettings'].gsub!(' --delete-input-file', '')
2610
2611 if @settings['other'] ; checkOtherSettings() end
2612
2613 # update the ripping settings for a hidden audio track if track 1 is selected
2614 if @settings['cd'].getStartSector(0) && @settings['tracksToRip'][0] == 1
2615 @settings['tracksToRip'].unshift(0)
2616 end
2617
2618 if @settings['req_matches_all'] > @settings['req_matches_errors'] ; @settings['req_matches_errors'] = @settings['req_matches_all'] end
2619 return true
2620 end
2621
2622 def checkOtherSettings
2623 copyString = ""
2624 lastChar = ""
2625
2626 #first remove all double quotes. then iterate over each char
2627 @settings['othersettings'].delete('"').split(//).each do |char|
2628 if char == '%' # prepend double quote before %
2629 copyString << '"' + char
2630 elsif lastChar == '%' # append double quote after %char
2631 copyString << char + '"'
2632 else
2633 copyString << char
2634 end
2635 lastChar = char
2636 end
2637
2638 # above won't work for various artist
2639 copyString.gsub!('"%v"a', '"%va"')
2640
2641 @settings['othersettings'] = copyString
2642
2643 puts @settings['othersettings'] if @settings['debug']
2644 end
2645
2646 def testDeps
2647 {"ripper" => "cdparanoia", "flac" => "flac", "vorbis" => "oggenc", "mp3" => "lame"}.each do |setting, binary|
2648 if @settings[setting] && !installed(binary)
2649 @error = ["error", _("%s not found on your system!") % [binary.capitalize]]
2650 return false
2651 end
2652 end
2653 return true
2654 end
2655
2656 def summary
2657 return @settings['log'].short_summary
2658 end
2659
2660 def postfixDir
2661 @settings['Out'].postfixDir()
2662 end
2663
2664 def overwriteDir
2665 @settings['Out'].overwriteDir()
2666 end
2667
2668 def updateGui
2669 @settings['log'].add(_("Cdrom player used to rip:\n%s\n") % [@settings['cd'].devicename])
2670 @settings['log'].add(_("Cdrom offset used: %s\n\n") % [@settings['offset']])
2671 @settings['log'].add(_("Ripper used: cdparanoia %s\n") % [if @settings['rippersettings'] ; @settings['rippersettings'] else _('default settings') end])
2672 @settings['log'].add(_("Matches required for all chunks: %s\n") % [@settings['req_matches_all']])
2673 @settings['log'].add(_("Matches required for erroneous chunks: %s\n\n") % [@settings['req_matches_errors']])
2674
2675 @settings['log'].add(_("Codec(s) used:\n"))
2676 if @settings['flac']; @settings['log'].add(_("-flac \t-> %s (%s)\n") % [@settings['flacsettings'], `flac --version`.strip]) end
2677 if @settings['vorbis']; @settings['log'].add(_("-vorbis\t-> %s (%s)\n") % [@settings['vorbissettings'], `oggenc --version`.strip]) end
2678 if @settings['mp3']; @settings['log'].add(_("-mp3\t-> %s\n(%s\n") % [@settings['mp3settings'], `lame --version`.split("\n")[0]]) end
2679 if @settings['wav']; @settings['log'].add(_("-wav\n")) end
2680 if @settings['other'] ; @settings['log'].add(_("-other\t-> %s\n") % [@settings['othersettings']]) end
2681 @settings['log'].add(_("\nCDDB INFO\n"))
2682 @settings['log'].add(_("\nArtist\t= "))
2683 @settings['log'].add(@settings['cd'].md.artist)
2684 @settings['log'].add(_("\nAlbum\t= "))
2685 @settings['log'].add(@settings['cd'].md.album)
2686 @settings['log'].add(_("\nYear\t= ") + @settings['cd'].md.year)
2687 @settings['log'].add(_("\nGenre\t= ") + @settings['cd'].md.genre)
2688 @settings['log'].add(_("\nTracks\t= ") + @settings['cd'].audiotracks.to_s +
2689 " (#{@settings['tracksToRip'].length} " + _("selected") + ")\n\n")
2690 @settings['cd'].audiotracks.times do |track|
2691 if @settings['tracksToRip'] == 'image' || @settings['tracksToRip'].include?(track + 1)
2692 @settings['log'].add("#{sprintf("%02d", track + 1)} - #{@settings['cd'].md.tracklist[track]}\n")
2693 end
2694 end
2695 end
2696
2697 def computePercentage
2698 @settings['percentages'] = Hash.new() #progress for each track
2699 totalSectors = 0.0 # It can be that the user doesn't want to rip all tracks, so calculate it
2700 @settings['tracksToRip'].each{|track| totalSectors += @settings['cd'].getLengthSector(track)} #update totalSectors
2701 @settings['tracksToRip'].each{|track| @settings['percentages'][track] = @settings['cd'].getLengthSector(track) / totalSectors}
2702 end
2703end