Class: Distillery::CLI
- Inherits:
-
Object
- Object
- Distillery::CLI
- Defined in:
- lib/distillery/cli.rb,
lib/distillery/cli/check.rb,
lib/distillery/cli/clean.rb,
lib/distillery/cli/index.rb,
lib/distillery/cli/header.rb,
lib/distillery/cli/rename.rb,
lib/distillery/cli/repack.rb,
lib/distillery/cli/overlap.rb,
lib/distillery/cli/rebuild.rb,
lib/distillery/cli/validate.rb
Constant Summary collapse
- OUTPUT_MODE =
List of available output mode
[ :text, :fancy, :json ]
- GlobalParser =
Global option parser
OptionParser.new do |opts| opts. = "Usage: #{opts.program_name} [options] CMD [opts] [args]" opts.separator "" opts.separator "Options:" opts.on "-h", "--help", "Show this message" do puts opts puts "" puts "Commands:" @@subcommands.each {|name, (desc, *) | puts " %-12s %s" % [ name, desc ] } puts "" puts "See '#{opts.program_name} CMD --help'" \ " for more information on a specific command" puts "" exit end opts.on "-V", "--version", "Show version" do puts opts.ver() exit end opts.separator "" opts.separator "Global options:" opts.on "-o", "--output=FILE", "Output file" opts.on "-m", "--output-mode=MODE", OUTPUT_MODE, "Output mode (#{OUTPUT_MODE.first})", " Value: #{OUTPUT_MODE.join(', ')}" opts.on "-d", "--dat=FILE", "DAT file" opts.on "-I", "--index=FILE", "Index file" opts.on "-D", "--destdir=DIR", "Destination directory" opts.on "-f", "--force", "Force operation" opts.on '-p', '--[no-]progress', "Show progress" opts.on '-v', '--[no-]verbose', "Run verbosely" end
- PROGNAME =
Program name
GlobalParser.program_name
- CheckParser =
Parser for check command
OptionParser.new do |opts| opts. = "Usage: #{PROGNAME} check [options] ROMDIR..." opts.separator "" opts.separator "Check ROMs status, and display missing or extra files." opts.separator "" opts.separator "Options:" opts.on '-r', '--revert', "Display present files instead" opts.separator "" end
- CleanParser =
Parser for clean command
OptionParser.new do |opts| opts. = "Usage: #{PROGNAME} clean [options] ROMDIR..." opts.separator "" opts.separator "Remove content not referenced in DAT file" opts.separator "" opts.separator "Options:" opts.on '-s', '--summarize', "Summarize results" opts.separator "" end
- IndexParser =
Parser for index command
OptionParser.new do |opts| opts. = "Usage: #{PROGNAME} index [options] ROMDIR..." opts.separator "" opts.separator "Generate hash index" opts.separator "" opts.separator "Options:" opts.on '-c', '--cksum=CHECKSUM', ROM::CHECKSUMS, "Checksum used for indexing (#{ROM::FS_CHECKSUM})", " Value: #{ROM::CHECKSUMS.join(', ')}" opts.on '-s', '--separator=CHAR', String, "Separator for archive entry (#{ROM::Path::Archive.separator})" opts.separator "" end
- HeaderParser =
Parser for header command
OptionParser.new do |opts| opts. = "Usage: #{PROGNAME} index ROMDIR..." opts.separator "" opts.separator "Extract ROM embedded header" opts.separator "" opts.separator "Options:" opts.separator "" end
- RepackParser =
Parser for repack command
OptionParser.new do |opts| types = ROMArchive::EXTENSIONS.to_a opts. = "Usage: #{PROGNAME} repack [options] ROMDIR..." opts.separator "" opts.separator "Repack archives to the specified format" opts.separator "" opts.separator "NOTE: if an archive in the new format already exists the operation" opts.separator " won't be carried out" opts.separator "" opts.separator "Options:" opts.on '-F', '--format=FORMAT', types, "Archive format (#{ROMArchive::PREFERED})", " Value: #{types.join(', ')}" opts.separator "" end
- OverlapParser =
Parser for overlap command
OptionParser.new do |opts| opts. = "Usage: #{PROGNAME} overlap [options] ROMDIR..." opts.separator "" opts.separator "Check ROMs status, and display missing or extra files." opts.separator "" opts.separator "Options:" opts.on '-r', '--revert', "Display present files instead" opts.separator "" end
- RebuildParser =
Parser for header command
OptionParser.new do |opts| opts. = "Usage: #{PROGNAME} rebuild ROMDIR..." end
- ValidateParser =
Parser for validate command
OptionParser.new do |opts| opts. = "Usage: #{PROGNAME} validate [options] ROMDIR..." opts.separator "" opts.separator "Validate ROMs according to DAT file" opts.separator "" opts.separator "Options:" opts.on '-s', '--summarize', "Summarize results" opts.separator "" end
Class Method Summary collapse
-
.run(argv = ARGV) ⇒ Object
Execute the CLI.
-
.subcommand(name, description, optparser = nil) {|argv, into:| ... } ⇒ Object
Register a new (sub)command into the CLI.
Instance Method Summary collapse
-
#check(datfile, romdirs, revert: false) ⇒ self
Check that the ROM directories form an exact match of the DAT file.
- #clean(datfile, romdirs, savedir: nil) ⇒ Object
-
#from_romdirs(romdirs, depth: nil) {|file, dir:| ... } ⇒ Object
Potential ROM from directory.
-
#header(hdrdir, romdirs) ⇒ self
Save ROM header in a specified directory.
-
#index(romdirs, type: nil, separator: nil) ⇒ self
Print index (hash and path of each ROM).
-
#initialize ⇒ CLI
constructor
A new instance of CLI.
-
#make_dat(file, verbose: @verbose, progress: @progress) ⇒ DatFile
Create DAT from file.
-
#make_storage(romdirs, depth: nil, verbose: @verbose, progress: @progress) ⇒ Storage
Create Storage from ROMs directories.
- #overlap(index, romdirs) ⇒ Object
-
#parse(argv) ⇒ Object
Parse command line arguments.
- #rebuild(gamedir, datfile, romdirs) ⇒ Object
- #rename(datfile, romdirs) ⇒ Object
- #repack(romdirs, type = nil) ⇒ Object
-
#validate(romdirs, datfile: nil, summarize: false) ⇒ self
Validate ROMs according to DAT/Index file.
Constructor Details
#initialize ⇒ CLI
Returns a new instance of CLI.
99 100 101 102 103 104 |
# File 'lib/distillery/cli.rb', line 99 def initialize @verbose = true @progress = true @output_mode = OUTPUT_MODE.first @io = $stdout end |
Class Method Details
.run(argv = ARGV) ⇒ Object
Execute the CLI
35 36 37 |
# File 'lib/distillery/cli.rb', line 35 def self.run(argv = ARGV) self.new.parse(argv) end |
.subcommand(name, description, optparser = nil) {|argv, into:| ... } ⇒ Object
Register a new (sub)command into the CLI
50 51 52 |
# File 'lib/distillery/cli.rb', line 50 def self.subcommand(name, description, optparser=nil, &exec) @@subcommands[name] = [ description, optparser, exec ] end |
Instance Method Details
#check(datfile, romdirs, revert: false) ⇒ self
Check that the ROM directories form an exact match of the DAT file
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# 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) ⇒ Object
7 8 9 10 11 12 13 14 |
# 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) {|file, dir:| ... } ⇒ Object
Potential ROM from directory.
186 187 188 189 190 |
# 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) ⇒ self
Save ROM header in a specified directory
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# 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) ⇒ self
Print index (hash and path of each ROM)
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# 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) ⇒ DatFile
Create DAT from file
167 168 169 170 171 172 173 |
# 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) ⇒ Storage
Create Storage from ROMs directories
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# 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) ⇒ Object
6 7 8 9 10 11 12 13 14 |
# 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) ⇒ Object
Parse command line arguments
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# 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) ⇒ Object
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# 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) ⇒ Object
6 7 8 9 10 11 |
# 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) ⇒ Object
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# 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) ⇒ self
Validate ROMs according to DAT/Index file.
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# 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 |