rom-distillery 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|