Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions lib/rubyripper/cli/cliPreferences.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,10 @@ def showSubMenuRipping
@out.puts ' 7) ' + _("Maximum trials") + ": %s" % [@prefs.maxTries == 0 ? "no\
maximum" : @prefs.maxTries]
@out.puts ' 8) ' + _("AccurateRip verification %s") % [showBool(@prefs.accRip)]
@out.puts ' 9) ' + _("AccurateRip every trial (Finish early if verified successfully) %s") % [showBool(@prefs.accRipEveryTime)]
@out.puts '10) ' + _("Eject disc after ripping %s") % [showBool(@prefs.eject)]
@out.puts '11) ' + _("Only keep log when errors %s") % [showBool(@prefs.noLog)]
@out.puts ' 9) ' + _("CTDB verification (image rip only) %s") % [showBool(@prefs.ctdb)]
@out.puts '10) ' + _("Verify every trial (Finish early if verified successfully) %s") % [showBool(@prefs.verifyEverytime)]
@out.puts '11) ' + _("Eject disc after ripping %s") % [showBool(@prefs.eject)]
@out.puts '12) ' + _("Only keep log when errors %s") % [showBool(@prefs.noLog)]
@out.puts '99) ' + _("Back to settings main menu")
@out.puts ""
@int.get("Please type the number of the setting you wish to change", 99)
Expand All @@ -168,9 +169,10 @@ def loopSubMenuRipping
when 6 then @prefs.reqMatchesErrors = @int.get(_("Match erronous chunks"), 3)
when 7 then @prefs.maxTries = @int.get(_("Maximum trials"), 5)
when 8 then switchBool('accRip')
when 9 then switchBool('accRipEveryTime')
when 10 then switchBool('eject')
when 11 then switchBool('noLog')
when 9 then switchBool('ctdb')
when 10 then switchBool('verifyEverytime')
when 11 then switchBool('eject')
when 12 then switchBool('noLog')
else noValidChoiceMessage(choice)
end
loopSubMenuRipping() unless choice == 99
Expand Down
251 changes: 251 additions & 0 deletions lib/rubyripper/ctdb.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
#!/usr/bin/env ruby
# Rubyripper - A secure ripper for Linux/BSD/OSX
# Copyright (C) 2007 - 2010 Bouke Woudstra (boukewoudstra@gmail.com)
#
# This file is part of Rubyripper. Rubyripper is free software: you can
# redistribute it and/or modify it under the terms of the GNU General
# Public License as published by the Free Software Foundation, either
# version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>

require 'rexml/document'
require 'tempfile'
require 'rubyripper/preferences/main'
require 'rubyripper/system/execute'
require 'rubyripper/disc/cuesheet'
require 'shellwords'

# Class to hold the verification results of CTDB
class CtdbResult
attr_reader :status, # boolean: verified OK or not
:confidence, # overall confidence value from database
:total_entries, # total number of entries found in database
:message, # additional message from ctdb-cli
:entries # array of database entries

def initialize
@status = false
@confidence = 0
@total_entries = 0
@message = nil
@entries = []
end

# Parse XML output from ctdb-cli verify command
def parseXml(xml_string)
return if xml_string.nil? || xml_string.empty?

begin
doc = REXML::Document.new(xml_string)
verify_element = doc.elements['ctdb/verify_result']
return unless verify_element

@message = verify_element.attributes['message']
@confidence = verify_element.attributes['confidence'].to_i
@total_entries = verify_element.attributes['total_entries'].to_i

# Determine status based on confidence and total_entries
# verified OK if (confidence >= 2) OR (confidence == 1 AND total_entries == 1)
@status = (@confidence >= 2) || (@confidence == 1 && @total_entries == 1)

# Parse entries
verify_element.elements.each('entry') do |entry|
entry_data = {
conf: entry.attributes['conf'].to_i,
crc: entry.attributes['crc'],
status: entry.attributes['status']
}
@entries << entry_data
end
rescue REXML::ParseException => e
@message = "XML parse error: #{e.message}"
@status = false
end
end

# Check if database entry was found (regardless of match result)
def entryFound?
@total_entries > 0 || !@entries.empty?
end

# Generate string for logging
def toStr
lines = []
lines << "CTDB verification results:"

if !entryFound?
lines << " No entry found in the database"
return lines.join("\n")
end

lines << " Status: #{@status ? 'Verification successful' : 'Verification failed'}"
lines << " Confidence: #{@confidence}/#{@total_entries}"
lines << " Message: #{@message}" if @message && !@message.empty?

unless @entries.empty?
lines << ""
lines << " Database entries:"
@entries.each_with_index do |entry, idx|
lines << " #{idx + 1}. Confidence: #{entry[:conf]}, CRC: #{entry[:crc]}, Status: #{entry[:status]}"
end
end

lines << ""

lines.join("\n")
end
end

# Class to perform CD verification using CTDB (CUETools Database)
# This class follows the same pattern as AccurateRip class
class Ctdb
def initialize(disc, cdrdao, fileScheme, prefs = nil, deps = nil, exec = nil)
@disc = disc
@cdrdao = cdrdao
@fileScheme = fileScheme
@prefs = prefs || Preferences::Main.instance
@deps = deps || Dependency.instance
@exec = exec || Execute.new
end

# Verify image file and return CtdbResult
# file_path: path to the WAV image file
def verifyImage(file_path)
result = CtdbResult.new

# Check if ctdb-cli is installed
unless @deps.installed?('ctdb-cli')
result.instance_variable_set(:@message, "ctdb-cli not installed")
return result
end

return result unless File.exist?(file_path)

# Generate temporary CUE sheet pointing to the image file
temp_cue = generateTempCue(file_path)
return result unless temp_cue

begin
# Execute ctdb-cli verify command
xml_output = executeVerify(temp_cue.path)
result.parseXml(xml_output)
ensure
# Clean up temporary CUE file
temp_cue.close
temp_cue.unlink
end

result
end

# Submit parity information to CTDB
# file_path: path to the WAV image file
# return true if submission was successful, false otherwise
def submit(file_path)
# Check if ctdb-cli is installed
unless @deps.installed?('ctdb-cli')
puts "CTDB: ctdb-cli not installed" if @prefs.debug
return false
end

return false unless File.exist?(file_path)

# Generate temporary CUE sheet pointing to the image file
temp_cue = generateTempCue(file_path)
return false unless temp_cue

begin
# Prepare drive name
drive_name = ""
scanner = @disc.advancedTocScanner
if scanner.respond_to?(:vendor) && scanner.vendor && scanner.model
drive_name = "#{scanner.vendor} - #{scanner.model}"
end

# Construct command
command = "ctdb-cli --xml submit #{Shellwords.escape(temp_cue.path)}"
command << " --drive #{Shellwords.escape(drive_name)} --quality 100"
# Set environment variable and execute
# Actual submission is enabled only if CTDB_CLI_CALLER is set.
# If in debug mode, we omit it to trigger ctdb-cli's dry-run mode.
env = {}
unless @prefs.debug
env['CTDB_CLI_CALLER'] = "RubyRipperRemix v.#{$rr_version}"
end

stdout_str, stderr_str, status = Open3.capture3(env, command)

if @prefs.debug
puts "CTDB submit command: #{command}"
puts "CTDB submit env: #{env.inspect}"
puts "CTDB submit stdout: #{stdout_str}"
puts "CTDB submit stderr: #{stderr_str}"
end

return false unless status.success?
return false if stdout_str.nil? || stdout_str.empty?

begin
doc = REXML::Document.new(stdout_str)
submit_element = doc.elements['ctdb/submit_result']
return false unless submit_element

res_status = submit_element.attributes['status']
if @prefs.debug
res_status == 'dry_run'
else
res_status == 'submitted' && submit_element.elements['response'].attributes['status'] == 'success'
end
rescue REXML::ParseException => e
puts "CTDB: XML parse error during submit: #{e.message}" if @prefs.debug
false
end
rescue StandardError => e
puts "CTDB: Error during submit: #{e.message}" if @prefs.debug
false
ensure
# Clean up temporary CUE file
temp_cue.close
temp_cue.unlink
end
end

private

# Generate a temporary CUE sheet pointing to the specified WAV file
def generateTempCue(wav_path)
# Create a cuesheet that references the temporary WAV file
cuesheet = Cuesheet.new(@disc, @cdrdao, @fileScheme, nil, @prefs, @deps)
cue_content = cuesheet.save('wav', wav_path)

# Create the temp CUE in the same directory as the wav_path provided.
dir = File.dirname(wav_path)
temp_cue = Tempfile.new(['ctdb_verify', '.cue'], dir)
temp_cue.write(cue_content.join("\n"))
temp_cue.flush

temp_cue
rescue StandardError => e
puts "CTDB: Error generating temp CUE: #{e.message}" if @prefs.debug
nil
end

# Execute ctdb-cli verify command and return XML output
def executeVerify(cue_path)
command = "ctdb-cli --xml verify #{Shellwords.escape(cue_path)}"
stdout_str, stderr_str, status = Open3.capture3(command)
puts "CTDB stderr: #{stderr_str}" if @prefs.debug && !stderr_str.empty?
stdout_str

rescue StandardError => e
puts "CTDB: Error executing ctdb-cli: #{e.message}" if @prefs.debug
nil
end
end
9 changes: 7 additions & 2 deletions lib/rubyripper/disc/cuesheet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,13 @@ def initialize(disc, cdrdao, fileScheme, fileAndDir=nil, prefs=nil, deps=nil)
end

# return an array with the cuesheet for a codec
def save(codec)
# single_file_path: optional path to use instead of FileScheme (for temp CUE generation)
def save(codec, single_file_path = nil)
@cuesheet = Array.new
@single_file_path = single_file_path
printDiscData
@prefs.image ? printTrackDataImage(codec) : printTrackData(codec)
@single_file_path = nil
@cuesheet
end

Expand Down Expand Up @@ -96,7 +99,9 @@ def printTrackDataImage(codec)

#writes the location of the file in the Cue
def printFileLine(codec, track=nil)
@cuesheet << "FILE \"#{File.basename(@fileScheme.getFile(codec, track))}\" #{getCueFileType(codec)}"
# Use single_file_path if set (for temp CUE generation), otherwise use FileScheme
file_path = @single_file_path || @fileScheme.getFile(codec, track)
@cuesheet << "FILE \"#{File.basename(file_path)}\" #{getCueFileType(codec)}"
end

def printTrackLine(track)
Expand Down
2 changes: 1 addition & 1 deletion lib/rubyripper/disc/disc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

# A helper class to hide lower level details
class Disc
attr_reader :metadata
attr_reader :metadata, :cdrdao

def initialize(cdpar=nil, freedb=nil, musicbrainz=nil, deps=nil, prefs=nil)
@cdparanoia = cdpar ? cdpar : ScanDiscCdparanoia.new()
Expand Down
3 changes: 2 additions & 1 deletion lib/rubyripper/disc/scanDiscCdinfo.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ class ScanDiscCdinfo
include AudioCalculations

attr_reader :status, :version, :discMode, :deviceName, :totalSectors,
:playtime, :audiotracks, :firstAudioTrack, :dataTracks
:playtime, :audiotracks, :firstAudioTrack, :dataTracks,
:vendor, :model

# Cd-info starts all tracks with 2 seconds extra if compared with cdparanoia
OFFSET_CDINFO = -150
Expand Down
25 changes: 19 additions & 6 deletions lib/rubyripper/gtk3/gtkPreferences.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ def loadPreferences
@errChunksSpin.value = @prefs.reqMatchesErrors.to_f
@maxSpin.value = @prefs.maxTries.to_f
@accRip.active = @prefs.accRip
@accRipEveryTime.active = @prefs.accRipEveryTime
@ctdb.active = @prefs.ctdb
@verifyEverytime.active = @prefs.verifyEverytime
@ripEntry.text = @prefs.rippersettings
@eject.active = @prefs.eject
@noLog.active = @prefs.noLog
Expand Down Expand Up @@ -169,7 +170,8 @@ def savePreferences
@prefs.reqMatchesErrors = @errChunksSpin.value.to_i
@prefs.maxTries = @maxSpin.value.to_i
@prefs.accRip = @accRip.active?
@prefs.accRipEveryTime = @accRipEveryTime.active?
@prefs.ctdb = @ctdb.active?
@prefs.verifyEverytime = @verifyEverytime.active?
@prefs.rippersettings = @ripEntry.text
@prefs.eject = @eject.active?
@prefs.noLog = @noLog.active?
Expand Down Expand Up @@ -292,13 +294,16 @@ def enablePaddingOption?

# 2nd frame on secure ripping tab
def buildFrameRippingOptions
@table50 = newTable(rows=5, columns=3)
@table50 = newTable(rows=6, columns=3)
#create objects
@all_chunks = Gtk::Label.new(_("Match all chunks:")) ; @all_chunks.set_alignment(0.0, 0.5)
@err_chunks = Gtk::Label.new(_("Match erroneous chunks:")) ; @err_chunks.set_alignment(0.0, 0.5)
@max_label = Gtk::Label.new(_("Maximum trials (0 = unlimited):")) ; @max_label.set_alignment(0.0, 0.5)
@accRip = Gtk::CheckButton.new(_("AccurateRip verification"))
@accRipEveryTime = Gtk::CheckButton.new(_("AccurateRip every trial (finish early on match)"))
@ctdb = Gtk::CheckButton.new(_("CTDB verification (image rip only)"))
@ctdb.tooltip_text = _("CUETools Database verification. Requires ctdb-cli to be installed.")
@ctdb.sensitive = @deps.installed?('ctdb-cli')
@verifyEverytime = Gtk::CheckButton.new(_("Verify every trial (finish early on match)"))
@allChunksSpin = Gtk::SpinButton.new(2.0, 100.0, 1.0)
@errChunksSpin = Gtk::SpinButton.new(2.0, 100.0, 1.0)
@maxSpin = Gtk::SpinButton.new(0.0, 100.0, 1.0)
Expand All @@ -310,7 +315,8 @@ def buildFrameRippingOptions
@table50.attach(@err_chunks, 0, 1, 1, 2, Gtk::AttachOptions::FILL, Gtk::AttachOptions::SHRINK, 0, 0)
@table50.attach(@max_label, 0, 1, 2, 3, Gtk::AttachOptions::FILL, Gtk::AttachOptions::SHRINK, 0, 0)
@table50.attach(@accRip, 0, 3, 3, 4, Gtk::AttachOptions::FILL, Gtk::AttachOptions::SHRINK, 0, 0)
@table50.attach(@accRipEveryTime, 0, 3, 4, 5, Gtk::AttachOptions::FILL, Gtk::AttachOptions::SHRINK, 0, 0)
@table50.attach(@ctdb, 0, 3, 4, 5, Gtk::AttachOptions::FILL, Gtk::AttachOptions::SHRINK, 0, 0)
@table50.attach(@verifyEverytime, 0, 3, 5, 6, Gtk::AttachOptions::FILL, Gtk::AttachOptions::SHRINK, 0, 0)

@table50.attach(@allChunksSpin, 1, 2, 0, 1, Gtk::AttachOptions::FILL, Gtk::AttachOptions::SHRINK, 0, 0) #2nd column
@table50.attach(@errChunksSpin, 1, 2, 1, 2, Gtk::AttachOptions::FILL, Gtk::AttachOptions::SHRINK, 0, 0)
Expand All @@ -320,10 +326,17 @@ def buildFrameRippingOptions
@table50.attach(@time3, 2, 3, 2, 3, Gtk::AttachOptions::FILL, Gtk::AttachOptions::SHRINK, 0, 0)
#connect a signal to @all_chunks to make sure @err_chunks get always at least the same amount of rips as @all_chunks
@allChunksSpin.signal_connect("value_changed") {if @errChunksSpin.value < @allChunksSpin.value ; @errChunksSpin.value = @allChunksSpin.value end ; @errChunksSpin.set_range(@allChunksSpin.value,100.0)} #ensure all_chunks cannot be smaller that err_chunks.
@accRip.signal_connect("clicked") {@accRipEveryTime.sensitive = @accRip.active?}
updateVerifyEverytimeSensitivity()
@accRip.signal_connect("clicked") { updateVerifyEverytimeSensitivity() }
@ctdb.signal_connect("clicked") { updateVerifyEverytimeSensitivity() }
@frame50= newFrame(_('Ripping options'), child=@table50)
end

# Update sensitivity of verifyEverytime based on accRip or ctdb being active
def updateVerifyEverytimeSensitivity
@verifyEverytime.sensitive = @accRip.active? || (@ctdb.active? && @deps.installed?('ctdb-cli'))
end

def buildFrameRippingRelated
@table60 = newTable(rows=2, columns=3)
#create objects
Expand Down
Loading