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.
@@ -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