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

new() click to toggle source
# File lib/distillery/cli.rb, line 99
def initialize
    @verbose     = true
    @progress    = true
    @output_mode = OUTPUT_MODE.first
    @io          = $stdout
end
run(argv = ARGV) click to toggle source

Execute the CLI

# File lib/distillery/cli.rb, line 35
def self.run(argv = ARGV)
    self.new.parse(argv)
end
subcommand(name, description, optparser=nil, &exec) click to toggle source

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(datfile, romdirs, revert: false) click to toggle source

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
clean(datfile, romdirs, savedir: nil) click to toggle source
# 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
from_romdirs(romdirs, depth: nil, &block) click to toggle source

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
header(hdrdir, romdirs) click to toggle source

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
index(romdirs, type: nil, separator: nil) click to toggle source

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
make_dat(file, verbose: @verbose, progress: @progress) click to toggle source

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
make_storage(romdirs, depth: nil, verbose: @verbose, progress: @progress) click to toggle source

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
overlap(index, romdirs) click to toggle source
# 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(argv) click to toggle source

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
rebuild(gamedir, datfile, romdirs) click to toggle source
# 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
rename(datfile, romdirs) click to toggle source
# 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
repack(romdirs, type = nil) click to toggle source
# 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(romdirs, datfile: nil, summarize: false) click to toggle source

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