class Distillery::CLI
Constants
- CheckParser
Parser for check command
- CleanParser
Parser for clean command
- GlobalParser
Global option parser
- HeaderParser
Parser for header command
- IndexParser
Parser for index command
- OUTPUT_MODE
List of available output mode
- OverlapParser
Parser for overlap command
- PROGNAME
Program name
- RebuildParser
Parser for header command
- RepackParser
Parser for repack command
- ValidateParser
Parser for validate command
Public Class Methods
# File lib/distillery/cli.rb, line 99 def initialize @verbose = true @progress = true @output_mode = OUTPUT_MODE.first @io = $stdout end
Execute the CLI
# File lib/distillery/cli.rb, line 35 def self.run(argv = ARGV) self.new.parse(argv) end
Register a new (sub)command into the CLI
@param name [Symbol] @param description [String] @param optpartser [OptionParser]
@yieldparam argv [Array<String>] @yieldparam into: [Object] @yieldreturn Array<Object> # Subcommand parameters
# File lib/distillery/cli.rb, line 50 def self.subcommand(name, description, optparser=nil, &exec) @@subcommands[name] = [ description, optparser, exec ] end
Public Instance Methods
Check that the ROM
directories form an exact match of the DAT file
@param datfile [String] DAT file @param romdirs [Array<String>] ROMs directories
@return [self]
# File lib/distillery/cli/check.rb, line 13 def check(datfile, romdirs, revert: false) dat = make_dat(datfile) storage = make_storage(romdirs) missing = dat.roms - storage.roms extra = storage.roms - dat.roms included = dat.roms & storage.roms printer = proc {|entry, subentries| @io.puts "- #{entry}" Array(subentries).each {|entry| @io.puts " . #{entry}" } } # Warn about presence of headered ROM if storage.headered warn "===> Headered ROM" end # Show included ROMs if revert if included.empty? @io.puts "==> No rom included" else @io.puts "==> Included roms (#{included.size}):" included.dump(comptact: true, &printer) end # Show mssing and extra ROMs else if ! missing.empty? @io.puts "==> Missing roms (#{missing.size}):" missing.dump(compact: true, &printer) end @io.puts if !missing.empty? && !extra.empty? if ! extra.empty? @io.puts "==> Extra roms (#{extra.size}):" extra.dump(compact: true, &printer) end end # Have we a perfect match ? if missing.empty? && extra.empty? @io.puts "==> PERFECT" end self end
# File lib/distillery/cli/clean.rb, line 7 def clean(datfile, romdirs, savedir: nil) dat = make_dat(datfile) storage = make_storage(romdirs) extra = storage.roms - dat.roms extra.save(savedir) if savedir extra.each {|rom| rom.delete! } end
Potential ROM
from directory. @see Vault.from_dir
for details
@param romdirs [Array<String>] path to rom directoris @param depth [Integer,nil] exploration depth
@yieldparam file [String] file being processed @yieldparam dir: [String] directory relative to
# File lib/distillery/cli.rb, line 186 def from_romdirs(romdirs, depth: nil, &block) romdirs.each {|dir| Vault.from_dir(dir, depth: depth, &block) } end
Save ROM
header in a specified directory
@param hdrdir [String] Directory for saving headers @param romdirs [Array<String>] ROMs directories
@return [self]
# File lib/distillery/cli/header.rb, line 13 def header(hdrdir, romdirs) storage = make_storage(romdirs) storage.roms.select {|rom| rom.headered? }.each {|rom| file = File.join(hdrdir, rom.fshash) header = rom.header if File.exists?(file) if header != File.binread(file) warn "different header exists : #{rom.fshash}" end next end File.write(file, header) } self end
Print index (hash and path of each ROM
)
@param romdirs [Array<String>] ROMs directories @param type [Symbol,nil] type of checksum to use
@return [self]
# File lib/distillery/cli/index.rb, line 13 def index(romdirs, type: nil, separator: nil) list = make_storage(romdirs).index(type, separator) if (@output_mode == :fancy) || (@output_mode == :text) list.each {|hash, path| @io.puts "#{hash} #{path}" } elsif @output_mode == :json @io.puts Hash[list.each.to_a].to_json else raise Assert end self end
Create DAT from file
@param file [String] dat file @param verbose [Boolean] be verbose
@return [DatFile]
# File lib/distillery/cli.rb, line 167 def make_dat(file, verbose: @verbose, progress: @progress) dat = DatFile.new(file) if verbose $stderr.puts "DAT = #{dat.version}" end dat end
Create Storage
from ROMs directories
@param romdirs [Array<String>] array of ROMs directories @param verbose [Boolean] be verbose
@return [Storage]
# File lib/distillery/cli.rb, line 200 def make_storage(romdirs, depth: nil, verbose: @verbose, progress: @progress) vault = Vault::new block = ->(file, dir:) { vault.add_from_file(file, dir) } if progress TTY::Spinner.new("[:spinner] :file", :hide_cursor => true, :clear => true) .run('Done!') {|spinner| from_romdirs(romdirs, depth: depth) {|file, dir:| width = TTY::Screen.width - 8 spinner.update(:file => file.ellipsize(width, :middle)) block.call(file, dir: dir) } } else from_romdirs(romdirs, depth: depth, &block) end Storage::new(vault) end
# File lib/distillery/cli/overlap.rb, line 6 def overlap(index, romdirs) index = Hash[File.readlines(index).map {|line| line.split(' ', 2) }] storage = make_storage(romdirs) storage.roms.select {|rom| index.include?(rom.sha1) } .each {|rom| @io.puts rom.path } end
Parse command line arguments
# File lib/distillery/cli.rb, line 110 def parse(argv) # Parsed option holder opts = {} # Parse global options GlobalParser.order!(argv, into: opts) # Check for subcommand subcommand = argv.shift&.to_sym if subcommand.nil? warn "subcommand missing" exit end if !@@subcommands.include?(subcommand) warn "subcommand \'#{subcommand}\' is not recognised" exit end # Process our options if opts.include?(:output) @io = File.open(opts[:output], File::CREAT|File::TRUNC|File::WRONLY) end if opts.include?(:verbose) @verbose = opts[:verbose] end if opts.include?(:progress) @progress = opts[:progress] end if opts.include?(:'output-mode') @output_mode = opts[:'output-mode'] end # Sanitize if (@ouput_mode == :fancy) && !@io.tty? @output_mode = :text end # Parse command, and build arguments call _, optparser, argbuilder = @@subcommands[subcommand] optparser.order!(argv, into: opts) if optparser args = argbuilder.call(argv, **opts) # Call subcommand self.method(subcommand).call(*args) rescue OptionParser::InvalidArgument => e warn "#{PROGNAME}: #{e}" end
# File lib/distillery/cli/rebuild.rb, line 6 def rebuild(gamedir, datfile, romdirs) dat = make_dat(datfile) storage = make_storage(*romdirs) # gamedir can be one of the romdir we must find a clever # way to avoid overwriting file romsdir = File.join(gamedir, '.roms') storage.build_roms_directory(romsdir, delete: true) vault = ROMVault.new vault.add_from_dir(romsdir) storage.build_games_archives(gamedir, dat, vault, '7z') FileUtils.remove_dir(romsdir) end
# File lib/distillery/cli/rename.rb, line 6 def rename(datfile, romdirs) dat = Distillery::DatFile.new(datfile) storage = create_storage(romdirs) storage.rename(dat) end
# File lib/distillery/cli/repack.rb, line 10 def repack(romdirs, type = nil) type ||= ROMArchive::PREFERED decorator = if @output_mode == :fancy lambda {|file, type, &block| spinner = TTY::Spinner.new("[:spinner] :file", :hide_cursor => true, :output => @io) width = TTY::Screen.width - 8 spinner.update(:file => file.ellipsize(width, :middle)) spinner.auto_spin case v = block.call when String then spinner.error("(#{v})") else spinner.success("-> #{type}") end } elsif @output_mode == :text lambda {|file, type, &block| case v = block.call when String @io.puts "FAILED: #{file} (#{v})" @io.puts "OK : #{file} -> #{type}" if @verbose end } else raise Assert end from_romdirs(romdirs) { | srcfile, dir: | # Destination file according to archive type dstfile = srcfile.dup dstfile += ".#{type}" unless dstfile.sub!(/\.[^.\/]*$/, ".#{type}") # Path for src and dst src = File.join(dir, srcfile) dst = File.join(dir, dstfile) # If source and destination are the same # - move source out of the way as we could recompress # using another algorithm if srcfile == dstfile phyfile = srcfile + '.' + SecureRandom.alphanumeric(10) phy = File.join(dir, phyfile) File.rename(src, phy) else phyfile = srcfile phy = src end # Recompress decorator.(srcfile, type) { next "#{type} exists" if File.exists?(dst) archive = Distillery::Archiver.for(dst) Distillery::Archiver.for(phy).each {|entry, i| archive.writer(entry) {|o| while data = i.read(32 * 1024) o.write(data) end } } File.unlink(phy) } } end
Validate ROMs according to DAT/Index file.
@param romdirs [Array<String>] ROMs directories @param datfile [String] DAT file
@return [self]
# File lib/distillery/cli/validate.rb, line 14 def validate(romdirs, datfile: nil, summarize: false) dat = make_dat(datfile) storage = make_storage(romdirs) count = { :not_found => 0, :name_mismatch => 0, :wrong_place => 0 } summarizer = lambda {|io| io.puts io.puts "Not found : #{count[:not_found ]}" io.puts "Name mismatch : #{count[:name_mismatch]}" io.puts "Wrong place : #{count[:wrong_place ]}" } checker = lambda {|game, rom| m = storage.roms.match(rom) if m.nil? || m.empty? count[:not_found] += 1 "not found" elsif (m = m.select {|r| r.name == rom.name }).empty? count[:name_mismatch] += 1 "name mismatch" elsif (m = m.select {|r| store = File.basename(r.path.storage) ROMArchive::EXTENSIONS.any? {|ext| ext = Regexp.escape(ext) store.gsub(/\.#{ext}$/i, '') == game.name } || (store == game.name) || romdirs.include?(store) }).empty? count[:wrong_place] += 1 "wrong place" end } if @output_mode == :fancy dat.each_game {|game| s_width = TTY::Screen.width r_width = s_width - 25 g_width = s_width - 10 game_name = game.name.ellipsize(g_width, :middle) gspinner = TTY::Spinner::Multi.new("[:spinner] #{game_name}", :hide_cursor => true, :output => @io) game.each_rom {|rom| rom_name = rom.name.ellipsize(r_width, :middle) rspinner = gspinner.register "[:spinner] :rom" rspinner.update(:rom => rom_name) rspinner.auto_spin case v = checker.(game, rom) when String then rspinner.error("-> #{v}") when nil then rspinner.success else raise Assert end } } if summarize summarize.(@io) end elsif (@output_mode == :text) && @verbose dat.each_game {|game| @io.puts "#{game}:" game.each_rom {|rom| case v = checker.(game, rom) when String then @io.puts " - FAILED: #{rom} -> #{v}" when nil then @io.puts " - OK : #{rom}" else raise Assert end } } if summarize summarize.(@io) end elsif @output_mode == :text dat.each_game.flat_map {|game| game.each_rom.map {|rom| case v = checker.(game, rom) when String then [ game.name, rom, v ] when nil else raise Assert end }.compact }.compact.group_by {|game,| game }.each {|game, list| @io.puts "#{game}" list.each {|_, rom, err| @io.puts " - FAILED: #{rom} -> #{err}" } } elsif @output_mode == :json @io.puts dat.each_game.map {|game| { :game => game.name, :roms => game.each_rom.map {|rom| case v = checker.(game, rom) when String, nil then [ game.name, rom, v ] else raise Assert end { :rom => rom.path.entry, :success => v.nil?, :reason => v }.compact } } }.to_json else raise Assert end self end