rom-distillery 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +6 -0
- data/LICENSE +287 -0
- data/README.md +24 -0
- data/bin/rhum +6 -0
- data/distillery.gemspec +38 -0
- data/lib/distillery.rb +10 -0
- data/lib/distillery/archiver.rb +372 -0
- data/lib/distillery/archiver/archive.rb +102 -0
- data/lib/distillery/archiver/external.rb +182 -0
- data/lib/distillery/archiver/external.yaml +31 -0
- data/lib/distillery/archiver/libarchive.rb +105 -0
- data/lib/distillery/archiver/zip.rb +88 -0
- data/lib/distillery/cli.rb +234 -0
- data/lib/distillery/cli/check.rb +100 -0
- data/lib/distillery/cli/clean.rb +60 -0
- data/lib/distillery/cli/header.rb +61 -0
- data/lib/distillery/cli/index.rb +65 -0
- data/lib/distillery/cli/overlap.rb +39 -0
- data/lib/distillery/cli/rebuild.rb +47 -0
- data/lib/distillery/cli/rename.rb +34 -0
- data/lib/distillery/cli/repack.rb +113 -0
- data/lib/distillery/cli/validate.rb +171 -0
- data/lib/distillery/datfile.rb +180 -0
- data/lib/distillery/error.rb +13 -0
- data/lib/distillery/game.rb +70 -0
- data/lib/distillery/game/release.rb +40 -0
- data/lib/distillery/refinements.rb +41 -0
- data/lib/distillery/rom-archive.rb +266 -0
- data/lib/distillery/rom.rb +585 -0
- data/lib/distillery/rom/path.rb +110 -0
- data/lib/distillery/rom/path/archive.rb +103 -0
- data/lib/distillery/rom/path/file.rb +100 -0
- data/lib/distillery/rom/path/virtual.rb +70 -0
- data/lib/distillery/storage.rb +170 -0
- data/lib/distillery/vault.rb +433 -0
- data/lib/distillery/version.rb +7 -0
- metadata +192 -0
@@ -0,0 +1,234 @@
|
|
1
|
+
# SPDX-License-Identifier: EUPL-1.2
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'json'
|
5
|
+
require 'tty/screen'
|
6
|
+
require 'tty/logger'
|
7
|
+
require 'tty/spinner'
|
8
|
+
require 'tty/spinner/multi'
|
9
|
+
require 'tty/progressbar'
|
10
|
+
|
11
|
+
require_relative 'storage'
|
12
|
+
require_relative 'datfile'
|
13
|
+
require_relative 'refinements'
|
14
|
+
|
15
|
+
|
16
|
+
if !defined?(::Version)
|
17
|
+
Version = Distillery::VERSION
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
module Distillery
|
22
|
+
|
23
|
+
class CLI
|
24
|
+
using Distillery::StringY
|
25
|
+
|
26
|
+
# List of available output mode
|
27
|
+
OUTPUT_MODE = [ :text, :fancy, :json ]
|
28
|
+
|
29
|
+
|
30
|
+
# @!visibility private
|
31
|
+
@@subcommands = {}
|
32
|
+
|
33
|
+
|
34
|
+
# Execute the CLI
|
35
|
+
def self.run(argv = ARGV)
|
36
|
+
self.new.parse(argv)
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
# Register a new (sub)command into the CLI
|
41
|
+
#
|
42
|
+
# @param name [Symbol]
|
43
|
+
# @param description [String]
|
44
|
+
# @param optpartser [OptionParser]
|
45
|
+
#
|
46
|
+
# @yieldparam argv [Array<String>]
|
47
|
+
# @yieldparam into: [Object]
|
48
|
+
# @yieldreturn Array<Object> # Subcommand parameters
|
49
|
+
#
|
50
|
+
def self.subcommand(name, description, optparser=nil, &exec)
|
51
|
+
@@subcommands[name] = [ description, optparser, exec ]
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
# Global option parser
|
56
|
+
#
|
57
|
+
GlobalParser = OptionParser.new do |opts|
|
58
|
+
opts.banner = "Usage: #{opts.program_name} [options] CMD [opts] [args]"
|
59
|
+
|
60
|
+
opts.separator ""
|
61
|
+
opts.separator "Options:"
|
62
|
+
opts.on "-h", "--help", "Show this message" do
|
63
|
+
puts opts
|
64
|
+
puts ""
|
65
|
+
puts "Commands:"
|
66
|
+
@@subcommands.each {|name, (desc, *) |
|
67
|
+
puts " %-12s %s" % [ name, desc ]
|
68
|
+
}
|
69
|
+
puts ""
|
70
|
+
puts "See '#{opts.program_name} CMD --help'" \
|
71
|
+
" for more information on a specific command"
|
72
|
+
puts ""
|
73
|
+
exit
|
74
|
+
end
|
75
|
+
|
76
|
+
opts.on "-V", "--version", "Show version" do
|
77
|
+
puts opts.ver()
|
78
|
+
exit
|
79
|
+
end
|
80
|
+
|
81
|
+
opts.separator ""
|
82
|
+
opts.separator "Global options:"
|
83
|
+
opts.on "-o", "--output=FILE", "Output file"
|
84
|
+
opts.on "-m", "--output-mode=MODE", OUTPUT_MODE,
|
85
|
+
"Output mode (#{OUTPUT_MODE.first})",
|
86
|
+
" Value: #{OUTPUT_MODE.join(', ')}"
|
87
|
+
opts.on "-d", "--dat=FILE", "DAT file"
|
88
|
+
opts.on "-I", "--index=FILE", "Index file"
|
89
|
+
opts.on "-D", "--destdir=DIR", "Destination directory"
|
90
|
+
opts.on "-f", "--force", "Force operation"
|
91
|
+
opts.on '-p', '--[no-]progress', "Show progress"
|
92
|
+
opts.on '-v', '--[no-]verbose', "Run verbosely"
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
# Program name
|
97
|
+
PROGNAME = GlobalParser.program_name
|
98
|
+
|
99
|
+
def initialize
|
100
|
+
@verbose = true
|
101
|
+
@progress = true
|
102
|
+
@output_mode = OUTPUT_MODE.first
|
103
|
+
@io = $stdout
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
# Parse command line arguments
|
108
|
+
#
|
109
|
+
#
|
110
|
+
def parse(argv)
|
111
|
+
# Parsed option holder
|
112
|
+
opts = {}
|
113
|
+
|
114
|
+
# Parse global options
|
115
|
+
GlobalParser.order!(argv, into: opts)
|
116
|
+
|
117
|
+
# Check for subcommand
|
118
|
+
subcommand = argv.shift&.to_sym
|
119
|
+
if subcommand.nil?
|
120
|
+
warn "subcommand missing"
|
121
|
+
exit
|
122
|
+
end
|
123
|
+
if !@@subcommands.include?(subcommand)
|
124
|
+
warn "subcommand \'#{subcommand}\' is not recognised"
|
125
|
+
exit
|
126
|
+
end
|
127
|
+
|
128
|
+
# Process our options
|
129
|
+
if opts.include?(:output)
|
130
|
+
@io = File.open(opts[:output],
|
131
|
+
File::CREAT|File::TRUNC|File::WRONLY)
|
132
|
+
end
|
133
|
+
if opts.include?(:verbose)
|
134
|
+
@verbose = opts[:verbose]
|
135
|
+
end
|
136
|
+
if opts.include?(:progress)
|
137
|
+
@progress = opts[:progress]
|
138
|
+
end
|
139
|
+
if opts.include?(:'output-mode')
|
140
|
+
@output_mode = opts[:'output-mode']
|
141
|
+
end
|
142
|
+
|
143
|
+
# Sanitize
|
144
|
+
if (@ouput_mode == :fancy) && !@io.tty?
|
145
|
+
@output_mode = :text
|
146
|
+
end
|
147
|
+
|
148
|
+
# Parse command, and build arguments call
|
149
|
+
_, optparser, argbuilder = @@subcommands[subcommand]
|
150
|
+
optparser.order!(argv, into: opts) if optparser
|
151
|
+
args = argbuilder.call(argv, **opts)
|
152
|
+
|
153
|
+
# Call subcommand
|
154
|
+
self.method(subcommand).call(*args)
|
155
|
+
rescue OptionParser::InvalidArgument => e
|
156
|
+
warn "#{PROGNAME}: #{e}"
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
# Create DAT from file
|
161
|
+
#
|
162
|
+
# @param file [String] dat file
|
163
|
+
# @param verbose [Boolean] be verbose
|
164
|
+
#
|
165
|
+
# @return [DatFile]
|
166
|
+
#
|
167
|
+
def make_dat(file, verbose: @verbose, progress: @progress)
|
168
|
+
dat = DatFile.new(file)
|
169
|
+
if verbose
|
170
|
+
$stderr.puts "DAT = #{dat.version}"
|
171
|
+
end
|
172
|
+
dat
|
173
|
+
end
|
174
|
+
|
175
|
+
|
176
|
+
|
177
|
+
# Potential ROM from directory.
|
178
|
+
# @see Vault.from_dir for details
|
179
|
+
#
|
180
|
+
# @param romdirs [Array<String>] path to rom directoris
|
181
|
+
# @param depth [Integer,nil] exploration depth
|
182
|
+
#
|
183
|
+
# @yieldparam file [String] file being processed
|
184
|
+
# @yieldparam dir: [String] directory relative to
|
185
|
+
#
|
186
|
+
def from_romdirs(romdirs, depth: nil, &block)
|
187
|
+
romdirs.each {|dir|
|
188
|
+
Vault.from_dir(dir, depth: depth, &block)
|
189
|
+
}
|
190
|
+
end
|
191
|
+
|
192
|
+
|
193
|
+
# Create Storage from ROMs directories
|
194
|
+
#
|
195
|
+
# @param romdirs [Array<String>] array of ROMs directories
|
196
|
+
# @param verbose [Boolean] be verbose
|
197
|
+
#
|
198
|
+
# @return [Storage]
|
199
|
+
#
|
200
|
+
def make_storage(romdirs, depth: nil,
|
201
|
+
verbose: @verbose, progress: @progress)
|
202
|
+
vault = Vault::new
|
203
|
+
block = ->(file, dir:) { vault.add_from_file(file, dir) }
|
204
|
+
|
205
|
+
if progress
|
206
|
+
TTY::Spinner.new("[:spinner] :file", :hide_cursor => true,
|
207
|
+
:clear => true)
|
208
|
+
.run('Done!') {|spinner|
|
209
|
+
from_romdirs(romdirs, depth: depth) {|file, dir:|
|
210
|
+
width = TTY::Screen.width - 8
|
211
|
+
spinner.update(:file => file.ellipsize(width, :middle))
|
212
|
+
block.call(file, dir: dir)
|
213
|
+
}
|
214
|
+
}
|
215
|
+
else
|
216
|
+
from_romdirs(romdirs, depth: depth, &block)
|
217
|
+
end
|
218
|
+
|
219
|
+
Storage::new(vault)
|
220
|
+
end
|
221
|
+
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
|
226
|
+
require_relative 'cli/check'
|
227
|
+
require_relative 'cli/validate'
|
228
|
+
require_relative 'cli/index'
|
229
|
+
require_relative 'cli/rename'
|
230
|
+
require_relative 'cli/rebuild'
|
231
|
+
require_relative 'cli/repack'
|
232
|
+
require_relative 'cli/overlap'
|
233
|
+
require_relative 'cli/header'
|
234
|
+
require_relative 'cli/clean'
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# SPDX-License-Identifier: EUPL-1.2
|
2
|
+
|
3
|
+
module Distillery
|
4
|
+
class CLI
|
5
|
+
|
6
|
+
# Check that the ROM directories form an exact match of the DAT file
|
7
|
+
#
|
8
|
+
# @param datfile [String] DAT file
|
9
|
+
# @param romdirs [Array<String>] ROMs directories
|
10
|
+
#
|
11
|
+
# @return [self]
|
12
|
+
#
|
13
|
+
def check(datfile, romdirs, revert: false)
|
14
|
+
dat = make_dat(datfile)
|
15
|
+
storage = make_storage(romdirs)
|
16
|
+
|
17
|
+
missing = dat.roms - storage.roms
|
18
|
+
extra = storage.roms - dat.roms
|
19
|
+
included = dat.roms & storage.roms
|
20
|
+
|
21
|
+
printer = proc {|entry, subentries|
|
22
|
+
@io.puts "- #{entry}"
|
23
|
+
Array(subentries).each {|entry| @io.puts " . #{entry}" }
|
24
|
+
}
|
25
|
+
|
26
|
+
# Warn about presence of headered ROM
|
27
|
+
if storage.headered
|
28
|
+
warn "===> Headered ROM"
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
# Show included ROMs
|
33
|
+
if revert
|
34
|
+
if included.empty?
|
35
|
+
@io.puts "==> No rom included"
|
36
|
+
else
|
37
|
+
@io.puts "==> Included roms (#{included.size}):"
|
38
|
+
included.dump(comptact: true, &printer)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Show mssing and extra ROMs
|
42
|
+
else
|
43
|
+
if ! missing.empty?
|
44
|
+
@io.puts "==> Missing roms (#{missing.size}):"
|
45
|
+
missing.dump(compact: true, &printer)
|
46
|
+
end
|
47
|
+
@io.puts if !missing.empty? && !extra.empty?
|
48
|
+
if ! extra.empty?
|
49
|
+
@io.puts "==> Extra roms (#{extra.size}):"
|
50
|
+
extra.dump(compact: true, &printer)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
# Have we a perfect match ?
|
56
|
+
if missing.empty? && extra.empty?
|
57
|
+
@io.puts "==> PERFECT"
|
58
|
+
end
|
59
|
+
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
# -----------------------------------------------------------------
|
65
|
+
|
66
|
+
|
67
|
+
# Parser for check command
|
68
|
+
CheckParser = OptionParser.new do |opts|
|
69
|
+
opts.banner = "Usage: #{PROGNAME} check [options] ROMDIR..."
|
70
|
+
|
71
|
+
opts.separator ""
|
72
|
+
opts.separator "Check ROMs status, and display missing or extra files."
|
73
|
+
opts.separator ""
|
74
|
+
opts.separator "Options:"
|
75
|
+
opts.on '-r', '--revert', "Display present files instead"
|
76
|
+
opts.separator ""
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
# Register check command
|
81
|
+
subcommand :check, "Check ROM status",
|
82
|
+
CheckParser do |argv, **opts|
|
83
|
+
opts[:romdirs] = argv
|
84
|
+
if opts[:dat].nil? && (opts[:romdirs].size >= 1)
|
85
|
+
opts[:dat] = File.join(opts[:romdirs].first, '.dat')
|
86
|
+
end
|
87
|
+
if opts[:dat].nil?
|
88
|
+
warn "missing datfile"
|
89
|
+
exit
|
90
|
+
end
|
91
|
+
if opts[:romdirs].empty?
|
92
|
+
warn "missing ROM directory"
|
93
|
+
exit
|
94
|
+
end
|
95
|
+
|
96
|
+
[ opts[:dat], opts[:romdirs], revert: opts[:revert] || false ]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# SPDX-License-Identifier: EUPL-1.2
|
2
|
+
|
3
|
+
module Distillery
|
4
|
+
|
5
|
+
class CLI
|
6
|
+
|
7
|
+
def clean(datfile, romdirs, savedir: nil)
|
8
|
+
dat = make_dat(datfile)
|
9
|
+
storage = make_storage(romdirs)
|
10
|
+
extra = storage.roms - dat.roms
|
11
|
+
|
12
|
+
extra.save(savedir) if savedir
|
13
|
+
extra.each {|rom| rom.delete! }
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
# -----------------------------------------------------------------
|
18
|
+
|
19
|
+
# Parser for clean command
|
20
|
+
CleanParser = OptionParser.new do |opts|
|
21
|
+
opts.banner = "Usage: #{PROGNAME} clean [options] ROMDIR..."
|
22
|
+
|
23
|
+
opts.separator ""
|
24
|
+
opts.separator "Remove content not referenced in DAT file"
|
25
|
+
opts.separator ""
|
26
|
+
opts.separator "Options:"
|
27
|
+
opts.on '-s', '--summarize', "Summarize results"
|
28
|
+
opts.separator ""
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
# Register clean command
|
33
|
+
subcommand :clean, "Remove content not referenced in DAT file",
|
34
|
+
CleanParser do |argv, **opts|
|
35
|
+
opts[:romdirs] = ARGV
|
36
|
+
if opts[:dat].nil? && (opts[:romdirs].size == 1)
|
37
|
+
opts[:dat] = File.join(opts[:romdirs].first, '.dat')
|
38
|
+
end
|
39
|
+
if opts[:savedir].nil? && (opts[:romdirs].size == 1)
|
40
|
+
opts[:savedir] = File.join(opts[:romdirs].first, '.trash')
|
41
|
+
end
|
42
|
+
|
43
|
+
if opts[:dat].nil?
|
44
|
+
warn "missing datfile"
|
45
|
+
exit
|
46
|
+
end
|
47
|
+
if opts[:romdirs].empty?
|
48
|
+
warn "missing ROM directory"
|
49
|
+
exit
|
50
|
+
end
|
51
|
+
if opts[:savedir].empty?
|
52
|
+
warn "missing save directory"
|
53
|
+
exit
|
54
|
+
end
|
55
|
+
|
56
|
+
[ opts[:dat], opts[:romdirs], savedir: opts[:savedir] ]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# SPDX-License-Identifier: EUPL-1.2
|
2
|
+
|
3
|
+
module Distillery
|
4
|
+
class CLI
|
5
|
+
|
6
|
+
# Save ROM header in a specified directory
|
7
|
+
#
|
8
|
+
# @param hdrdir [String] Directory for saving headers
|
9
|
+
# @param romdirs [Array<String>] ROMs directories
|
10
|
+
#
|
11
|
+
# @return [self]
|
12
|
+
#
|
13
|
+
def header(hdrdir, romdirs)
|
14
|
+
storage = make_storage(romdirs)
|
15
|
+
storage.roms.select {|rom| rom.headered? }.each {|rom|
|
16
|
+
file = File.join(hdrdir, rom.fshash)
|
17
|
+
header = rom.header
|
18
|
+
if File.exists?(file)
|
19
|
+
if header != File.binread(file)
|
20
|
+
warn "different header exists : #{rom.fshash}"
|
21
|
+
end
|
22
|
+
next
|
23
|
+
end
|
24
|
+
File.write(file, header)
|
25
|
+
}
|
26
|
+
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
# -----------------------------------------------------------------
|
32
|
+
|
33
|
+
|
34
|
+
# Parser for header command
|
35
|
+
HeaderParser = OptionParser.new do |opts|
|
36
|
+
opts.banner = "Usage: #{PROGNAME} index ROMDIR..."
|
37
|
+
|
38
|
+
opts.separator ""
|
39
|
+
opts.separator "Extract ROM embedded header"
|
40
|
+
opts.separator ""
|
41
|
+
opts.separator "Options:"
|
42
|
+
opts.separator ""
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
# Register header command
|
47
|
+
subcommand :header, "Extract ROM embedded header",
|
48
|
+
HeaderParser do |argv, **opts|
|
49
|
+
opts[:romdirs] = ARGV
|
50
|
+
if opts[:destdir].nil? && (opts[:romdirs].size == 1)
|
51
|
+
opts[:destdir] = File.join(opts[:romdirs].first, '.header')
|
52
|
+
end
|
53
|
+
if opts[:romdirs].empty?
|
54
|
+
warn "missing ROM directory"
|
55
|
+
exit
|
56
|
+
end
|
57
|
+
|
58
|
+
[ opts[:destdir], opts[:romdirs] ]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|