rom-distillery 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,65 @@
1
+ # SPDX-License-Identifier: EUPL-1.2
2
+
3
+ module Distillery
4
+ class CLI
5
+
6
+ # Print index (hash and path of each ROM)
7
+ #
8
+ # @param romdirs [Array<String>] ROMs directories
9
+ # @param type [Symbol,nil] type of checksum to use
10
+ #
11
+ # @return [self]
12
+ #
13
+ def index(romdirs, type: nil, separator: nil)
14
+ list = make_storage(romdirs).index(type, separator)
15
+
16
+ if (@output_mode == :fancy) || (@output_mode == :text)
17
+ list.each {|hash, path|
18
+ @io.puts "#{hash} #{path}"
19
+ }
20
+
21
+ elsif @output_mode == :json
22
+ @io.puts Hash[list.each.to_a].to_json
23
+
24
+ else
25
+ raise Assert
26
+ end
27
+
28
+ self
29
+ end
30
+
31
+
32
+ # -----------------------------------------------------------------
33
+
34
+
35
+ # Parser for index command
36
+ IndexParser = OptionParser.new do |opts|
37
+ opts.banner = "Usage: #{PROGNAME} index [options] ROMDIR..."
38
+
39
+ opts.separator ""
40
+ opts.separator "Generate hash index"
41
+ opts.separator ""
42
+ opts.separator "Options:"
43
+ opts.on '-c', '--cksum=CHECKSUM', ROM::CHECKSUMS,
44
+ "Checksum used for indexing (#{ROM::FS_CHECKSUM})",
45
+ " Value: #{ROM::CHECKSUMS.join(', ')}"
46
+ opts.on '-s', '--separator=CHAR', String,
47
+ "Separator for archive entry (#{ROM::Path::Archive.separator})"
48
+ opts.separator ""
49
+ end
50
+
51
+
52
+ # Register index command
53
+ subcommand :index, "Generate hash index",
54
+ IndexParser do |argv, **opts|
55
+
56
+ if argv.empty?
57
+ warn "At least one rom directory is required"
58
+ exit
59
+ end
60
+
61
+ [ argv, type: opts[:cksum], separator: opts[:separator] ]
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,39 @@
1
+ # SPDX-License-Identifier: EUPL-1.2
2
+
3
+ module Distillery
4
+ class CLI
5
+
6
+ def overlap(index, romdirs)
7
+ index = Hash[File.readlines(index).map {|line| line.split(' ', 2) }]
8
+ storage = make_storage(romdirs)
9
+ storage.roms.select {|rom| index.include?(rom.sha1) }
10
+ .each {|rom|
11
+ @io.puts rom.path
12
+ }
13
+
14
+ end
15
+
16
+
17
+ # -----------------------------------------------------------------
18
+
19
+ # Parser for overlap command
20
+ OverlapParser = OptionParser.new do |opts|
21
+ opts.banner = "Usage: #{PROGNAME} overlap [options] ROMDIR..."
22
+
23
+ opts.separator ""
24
+ opts.separator "Check ROMs status, and display missing or extra files."
25
+ opts.separator ""
26
+ opts.separator "Options:"
27
+ opts.on '-r', '--revert', "Display present files instead"
28
+ opts.separator ""
29
+ end
30
+
31
+
32
+ # Register overlap command
33
+ subcommand :overlap, "Check for overlaping ROM" do |argv, **opts|
34
+ opts[:romdirs] = argv
35
+
36
+ [ opts[:index], opts[:romdirs] ]
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,47 @@
1
+ # SPDX-License-Identifier: EUPL-1.2
2
+
3
+ module Distillery
4
+ class CLI
5
+
6
+ def rebuild(gamedir, datfile, romdirs)
7
+ dat = make_dat(datfile)
8
+ storage = make_storage(*romdirs)
9
+
10
+ # gamedir can be one of the romdir we must find a clever
11
+ # way to avoid overwriting file
12
+
13
+ romsdir = File.join(gamedir, '.roms')
14
+ storage.build_roms_directory(romsdir, delete: true)
15
+
16
+ vault = ROMVault.new
17
+ vault.add_from_dir(romsdir)
18
+
19
+ storage.build_games_archives(gamedir, dat, vault, '7z')
20
+ FileUtils.remove_dir(romsdir)
21
+ end
22
+
23
+ # -----------------------------------------------------------------
24
+
25
+
26
+ # Parser for header command
27
+ RebuildParser = OptionParser.new do |opts|
28
+ opts.banner = "Usage: #{PROGNAME} rebuild ROMDIR..."
29
+ end
30
+
31
+
32
+ # Register rebuild command
33
+ subcommand :rebuild, "Rebuild according to DAT file",
34
+ RebuildParser 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
+
40
+ if opts[:destdir].nil? && (opts[:romdirs].size >= 1)
41
+ opts[:destdir] = opts[:romdirs].first
42
+ end
43
+
44
+ [ opts[:destdir], opts[:dat], opts[:romdirs] ]
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,34 @@
1
+ # SPDX-License-Identifier: EUPL-1.2
2
+
3
+ module Distillery
4
+ class CLI
5
+
6
+ def rename(datfile, romdirs)
7
+ dat = Distillery::DatFile.new(datfile)
8
+ storage = create_storage(romdirs)
9
+
10
+ storage.rename(dat)
11
+ end
12
+
13
+ # -----------------------------------------------------------------
14
+
15
+
16
+ # Register rename command
17
+ subcommand :rename, "Rename ROMs according to DAT" do |argv, **opts|
18
+ opts[:romdirs] = argv
19
+ if opts[:dat].nil? && (opts[:romdirs].size == 1)
20
+ opts[:dat] = File.join(opts[:romdirs].first, '.dat')
21
+ end
22
+ if opts[:dat].nil?
23
+ warn "missing datfile"
24
+ exit
25
+ end
26
+ if opts[:romdirs].empty?
27
+ warn "missing ROM directory"
28
+ exit
29
+ end
30
+
31
+ [ opts[:dat], opts[:romdirs] ]
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,113 @@
1
+ # coding: utf-8
2
+ # SPDX-License-Identifier: EUPL-1.2
3
+
4
+ require 'securerandom'
5
+
6
+ module Distillery
7
+ class CLI
8
+ using Distillery::StringY
9
+
10
+ def repack(romdirs, type = nil)
11
+ type ||= ROMArchive::PREFERED
12
+
13
+ decorator =
14
+ if @output_mode == :fancy
15
+ lambda {|file, type, &block|
16
+ spinner = TTY::Spinner.new("[:spinner] :file",
17
+ :hide_cursor => true,
18
+ :output => @io)
19
+ width = TTY::Screen.width - 8
20
+ spinner.update(:file => file.ellipsize(width, :middle))
21
+ spinner.auto_spin
22
+ case v = block.call
23
+ when String then spinner.error("(#{v})")
24
+ else spinner.success("-> #{type}")
25
+ end
26
+ }
27
+
28
+ elsif @output_mode == :text
29
+ lambda {|file, type, &block|
30
+ case v = block.call
31
+ when String
32
+ @io.puts "FAILED: #{file} (#{v})"
33
+ @io.puts "OK : #{file} -> #{type}" if @verbose
34
+ end
35
+ }
36
+
37
+ else
38
+
39
+ raise Assert
40
+ end
41
+
42
+
43
+ from_romdirs(romdirs) { | srcfile, dir: |
44
+ # Destination file according to archive type
45
+ dstfile = srcfile.dup
46
+ dstfile += ".#{type}" unless dstfile.sub!(/\.[^.\/]*$/, ".#{type}")
47
+
48
+ # Path for src and dst
49
+ src = File.join(dir, srcfile)
50
+ dst = File.join(dir, dstfile)
51
+
52
+ # If source and destination are the same
53
+ # - move source out of the way as we could recompress
54
+ # using another algorithm
55
+ if srcfile == dstfile
56
+ phyfile = srcfile + '.' + SecureRandom.alphanumeric(10)
57
+ phy = File.join(dir, phyfile)
58
+ File.rename(src, phy)
59
+ else
60
+ phyfile = srcfile
61
+ phy = src
62
+ end
63
+
64
+ # Recompress
65
+ decorator.(srcfile, type) {
66
+ next "#{type} exists" if File.exists?(dst)
67
+ archive = Distillery::Archiver.for(dst)
68
+ Distillery::Archiver.for(phy).each {|entry, i|
69
+ archive.writer(entry) {|o|
70
+ while data = i.read(32 * 1024)
71
+ o.write(data)
72
+ end
73
+ }
74
+ }
75
+ File.unlink(phy)
76
+ }
77
+ }
78
+ end
79
+
80
+
81
+ # -----------------------------------------------------------------
82
+
83
+
84
+ # Parser for repack command
85
+ RepackParser = OptionParser.new do |opts|
86
+ types = ROMArchive::EXTENSIONS.to_a
87
+ opts.banner = "Usage: #{PROGNAME} repack [options] ROMDIR..."
88
+
89
+ opts.separator ""
90
+ opts.separator "Repack archives to the specified format"
91
+ opts.separator ""
92
+ opts.separator "NOTE: if an archive in the new format already exists the operation"
93
+
94
+ opts.separator " won't be carried out"
95
+ opts.separator ""
96
+ opts.separator "Options:"
97
+ opts.on '-F', '--format=FORMAT', types,
98
+ "Archive format (#{ROMArchive::PREFERED})",
99
+ " Value: #{types.join(', ')}"
100
+ opts.separator ""
101
+ end
102
+
103
+
104
+ # Register repack command
105
+ subcommand :repack, "Recompress archives",
106
+ RepackParser do |argv, **opts|
107
+ opts[:romdirs] = argv
108
+
109
+ [ opts[:romdirs], opts[:format] ]
110
+ end
111
+
112
+ end
113
+ end
@@ -0,0 +1,171 @@
1
+ # SPDX-License-Identifier: EUPL-1.2
2
+
3
+ module Distillery
4
+ class CLI
5
+ using Distillery::StringY
6
+
7
+ # Validate ROMs according to DAT/Index file.
8
+ #
9
+ # @param romdirs [Array<String>] ROMs directories
10
+ # @param datfile [String] DAT file
11
+ #
12
+ # @return [self]
13
+ #
14
+ def validate(romdirs, datfile: nil, summarize: false)
15
+ dat = make_dat(datfile)
16
+ storage = make_storage(romdirs)
17
+ count = { :not_found => 0,
18
+ :name_mismatch => 0,
19
+ :wrong_place => 0 }
20
+ summarizer = lambda {|io|
21
+ io.puts
22
+ io.puts "Not found : #{count[:not_found ]}"
23
+ io.puts "Name mismatch : #{count[:name_mismatch]}"
24
+ io.puts "Wrong place : #{count[:wrong_place ]}"
25
+ }
26
+ checker = lambda {|game, rom|
27
+ m = storage.roms.match(rom)
28
+
29
+ if m.nil? || m.empty?
30
+ count[:not_found] += 1
31
+ "not found"
32
+ elsif (m = m.select {|r| r.name == rom.name }).empty?
33
+ count[:name_mismatch] += 1
34
+ "name mismatch"
35
+ elsif (m = m.select {|r|
36
+ store = File.basename(r.path.storage)
37
+ ROMArchive::EXTENSIONS.any? {|ext|
38
+ ext = Regexp.escape(ext)
39
+ store.gsub(/\.#{ext}$/i, '') == game.name
40
+ } || (store == game.name) || romdirs.include?(store)
41
+ }).empty?
42
+ count[:wrong_place] += 1
43
+ "wrong place"
44
+ end
45
+ }
46
+
47
+
48
+
49
+ if @output_mode == :fancy
50
+ dat.each_game {|game|
51
+ s_width = TTY::Screen.width
52
+ r_width = s_width - 25
53
+ g_width = s_width - 10
54
+
55
+ game_name = game.name.ellipsize(g_width, :middle)
56
+ gspinner = TTY::Spinner::Multi.new("[:spinner] #{game_name}",
57
+ :hide_cursor => true,
58
+ :output => @io)
59
+
60
+ game.each_rom {|rom|
61
+ rom_name = rom.name.ellipsize(r_width, :middle)
62
+ rspinner = gspinner.register "[:spinner] :rom"
63
+ rspinner.update(:rom => rom_name)
64
+ rspinner.auto_spin
65
+
66
+ case v = checker.(game, rom)
67
+ when String then rspinner.error("-> #{v}")
68
+ when nil then rspinner.success
69
+ else raise Assert
70
+ end
71
+ }
72
+ }
73
+ if summarize
74
+ summarize.(@io)
75
+ end
76
+
77
+
78
+ elsif (@output_mode == :text) && @verbose
79
+ dat.each_game {|game|
80
+ @io.puts "#{game}:"
81
+ game.each_rom {|rom|
82
+ case v = checker.(game, rom)
83
+ when String then @io.puts " - FAILED: #{rom} -> #{v}"
84
+ when nil then @io.puts " - OK : #{rom}"
85
+ else raise Assert
86
+ end
87
+ }
88
+ }
89
+ if summarize
90
+ summarize.(@io)
91
+ end
92
+
93
+ elsif @output_mode == :text
94
+ dat.each_game.flat_map {|game|
95
+ game.each_rom.map {|rom|
96
+ case v = checker.(game, rom)
97
+ when String then [ game.name, rom, v ]
98
+ when nil
99
+ else raise Assert
100
+ end
101
+ }.compact
102
+ }.compact.group_by {|game,| game }.each {|game, list|
103
+ @io.puts "#{game}"
104
+ list.each {|_, rom, err|
105
+ @io.puts " - FAILED: #{rom} -> #{err}"
106
+ }
107
+ }
108
+
109
+ elsif @output_mode == :json
110
+ @io.puts dat.each_game.map {|game|
111
+ { :game => game.name,
112
+ :roms => game.each_rom.map {|rom|
113
+ case v = checker.(game, rom)
114
+ when String, nil then [ game.name, rom, v ]
115
+ else raise Assert
116
+ end
117
+ { :rom => rom.path.entry,
118
+ :success => v.nil?,
119
+ :reason => v
120
+ }.compact
121
+ }
122
+ }
123
+ }.to_json
124
+ else
125
+
126
+ raise Assert
127
+ end
128
+
129
+ self
130
+ end
131
+
132
+
133
+ # -----------------------------------------------------------------
134
+
135
+
136
+ # Parser for validate command
137
+ ValidateParser = OptionParser.new do |opts|
138
+ opts.banner = "Usage: #{PROGNAME} validate [options] ROMDIR..."
139
+
140
+ opts.separator ""
141
+ opts.separator "Validate ROMs according to DAT file"
142
+ opts.separator ""
143
+ opts.separator "Options:"
144
+ opts.on '-s', '--summarize', "Summarize results"
145
+ opts.separator ""
146
+
147
+ end
148
+
149
+
150
+ # Register validate command
151
+ subcommand :validate, "Validate ROMs according to DAT file",
152
+ ValidateParser do |argv, **opts|
153
+ opts[:romdirs] = argv
154
+
155
+ if opts[:dat].nil? && (opts[:romdirs].size >= 1)
156
+ opts[:dat] = File.join(opts[:romdirs].first, '.dat')
157
+ end
158
+ if opts[:dat].nil?
159
+ warn "missing datfile"
160
+ exit
161
+ end
162
+ if opts[:romdirs].empty?
163
+ warn "missing ROM directory"
164
+ exit
165
+ end
166
+
167
+ [ opts[:romdirs], datfile: opts[:dat] ]
168
+ end
169
+ end
170
+
171
+ end