gamerom 0.6.0 → 0.7.0
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 +4 -4
- data/.gitignore +3 -0
- data/CHANGELOG.md +10 -1
- data/Gemfile.lock +1 -1
- data/README.md +2 -2
- data/gamerom.gemspec +0 -1
- data/lib/gamerom/cli.rb +33 -52
- data/lib/gamerom/repo.rb +8 -2
- data/lib/gamerom/repo_adapter.rb +15 -1
- data/lib/gamerom/repo_adapters/edgeemu.rb +8 -3
- data/lib/gamerom/repo_adapters/vimm.rb +21 -6
- data/lib/gamerom/version.rb +1 -1
- metadata +1 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 597dfe815ad760060e224b1320e8c2fe3684ced9f41e722e14470a5861a5a4a1
|
|
4
|
+
data.tar.gz: 417408b1b42bdc2c01f55a1878e46d63052e3c74d4a829775d8378aaeb024a33
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1de1c51f9f57a6baeaca2ae3943ab4564e622d5857ca433a322bbc2022de5e9465c26711f4c92a7efeafa6996c82c89618fb165da51c0aed7b7fccd04f52e594
|
|
7
|
+
data.tar.gz: eaafa9d35704dbc2ebf352cc31d5ae7a20e964f3c07ecdc3a15c3ca914728ecf836948354758e2b000fef9eded8cba00b339f2308aaef2f3d2a1290512e77e22
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.7.0] - 2026-11-01
|
|
10
|
+
- feat: recover - match files by normalized name and tags, so re-releases such as "Mega Man 2 (Capcom Town)" and editions such as "Jeopardy! Deluxe Edition" no longer end up in the plain game's state
|
|
11
|
+
- feat: edgeemu and vimm repos - multi-region releases that include USA stay under USA, otherwise the alphabetically first region
|
|
12
|
+
- feat: vimm repo - read the game region from the flags shown on the list pages instead of assuming USA
|
|
13
|
+
- fix: vimm repo - list pages gained a media icon before the title link, which made update_database write empty databases
|
|
14
|
+
- fix: update_database refuses to replace a populated database with an empty crawl
|
|
15
|
+
- fix: honor Retry-After on 429 responses and keep updating the other databases when one platform fails
|
|
16
|
+
|
|
9
17
|
## [0.6.0] - 2026-11-01
|
|
10
18
|
- feat: add edgeemu repo (https://edgeemu.net), 48 platforms
|
|
11
19
|
- feat: recover - do not claim files that belong to a game with a longer name (e.g. "Kung-Fu" vs "Kung-Fu Master")
|
|
@@ -53,7 +61,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
53
61
|
## [0.1.0] - 2021-05-07
|
|
54
62
|
- Initial release
|
|
55
63
|
|
|
56
|
-
[Unreleased]: https://github.com/lucasmundim/gamerom/compare/v0.
|
|
64
|
+
[Unreleased]: https://github.com/lucasmundim/gamerom/compare/v0.7.0...HEAD
|
|
65
|
+
[0.7.0]: https://github.com/lucasmundim/gamerom/compare/v0.6.0...v0.7.0
|
|
57
66
|
[0.6.0]: https://github.com/lucasmundim/gamerom/compare/v0.5.0...v0.6.0
|
|
58
67
|
[0.5.0]: https://github.com/lucasmundim/gamerom/compare/v0.4.1...v0.5.0
|
|
59
68
|
[0.4.1]: https://github.com/lucasmundim/gamerom/compare/v0.4.0...v0.4.1
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -15,7 +15,7 @@ It currently supports the following repositories:
|
|
|
15
15
|
Add this to your .bashrc, .bash_aliases, etc:
|
|
16
16
|
|
|
17
17
|
```
|
|
18
|
-
alias gamerom="docker run -e UID=$(id -u) -e GROUP_ID=$(id -g) --rm -it -v ~/.gamerom:/home/runuser/.gamerom --platform=linux/amd64 docker.io/lucasmundim/gamerom:0.
|
|
18
|
+
alias gamerom="docker run -e UID=$(id -u) -e GROUP_ID=$(id -g) --rm -it -v ~/.gamerom:/home/runuser/.gamerom --platform=linux/amd64 docker.io/lucasmundim/gamerom:0.7.0"
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
Use it as:
|
|
@@ -618,7 +618,7 @@ Print program version
|
|
|
618
618
|
|
|
619
619
|
```
|
|
620
620
|
$ gamerom version
|
|
621
|
-
0.
|
|
621
|
+
0.7.0
|
|
622
622
|
```
|
|
623
623
|
|
|
624
624
|
## Development
|
data/gamerom.gemspec
CHANGED
|
@@ -16,7 +16,6 @@ Gem::Specification.new do |spec|
|
|
|
16
16
|
|
|
17
17
|
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
|
18
18
|
|
|
19
|
-
spec.metadata['homepage_uri'] = spec.homepage
|
|
20
19
|
spec.metadata['source_code_uri'] = spec.homepage
|
|
21
20
|
spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/master/CHANGELOG.md"
|
|
22
21
|
spec.metadata['rubygems_mfa_required'] = 'true'
|
data/lib/gamerom/cli.rb
CHANGED
|
@@ -124,8 +124,8 @@ module Gamerom
|
|
|
124
124
|
validate_platform repo, options[:platform]
|
|
125
125
|
puts "recovering state of roms for #{options[:platform]} platform on #{options[:repo]} repo..."
|
|
126
126
|
games = repo.games options[:platform]
|
|
127
|
-
|
|
128
|
-
results = games.group_by { |game| recover_game(game,
|
|
127
|
+
parsed_names = games.to_h { |game| [game.id, parse_rom_name(game.name)] }
|
|
128
|
+
results = games.group_by { |game| recover_game(game, parsed_names) }
|
|
129
129
|
|
|
130
130
|
print_recover_summary(results.fetch(:found, []), results.fetch(:already_installed, []), results.fetch(:not_found, []))
|
|
131
131
|
rescue StandardError => e
|
|
@@ -268,11 +268,19 @@ module Gamerom
|
|
|
268
268
|
def update_all_databases
|
|
269
269
|
puts "updating all databases on #{options[:repo]} repo..."
|
|
270
270
|
repo = Repo.new(options[:repo])
|
|
271
|
-
repo.platforms.
|
|
272
|
-
repo = Repo.new(options[:repo])
|
|
271
|
+
failed = repo.platforms.keys.reject do |platform|
|
|
273
272
|
repo.update_database platform
|
|
273
|
+
true
|
|
274
|
+
rescue StandardError => e
|
|
275
|
+
shell.say "Error updating #{platform}: #{e.message}", :red
|
|
276
|
+
false
|
|
277
|
+
end
|
|
278
|
+
if failed.empty?
|
|
279
|
+
shell.say 'All game databases updated', :green
|
|
280
|
+
else
|
|
281
|
+
shell.say "Failed to update: #{failed.join(", ")}", :red
|
|
282
|
+
exit 1
|
|
274
283
|
end
|
|
275
|
-
shell.say 'All game databases updated', :green
|
|
276
284
|
rescue StandardError => e
|
|
277
285
|
render_error e, options
|
|
278
286
|
exit 1
|
|
@@ -327,7 +335,7 @@ module Gamerom
|
|
|
327
335
|
|
|
328
336
|
# Recover helpers
|
|
329
337
|
|
|
330
|
-
def recover_game(game,
|
|
338
|
+
def recover_game(game, parsed_names)
|
|
331
339
|
puts
|
|
332
340
|
puts "Checking game #{game}"
|
|
333
341
|
if game.filenames.any?
|
|
@@ -335,7 +343,7 @@ module Gamerom
|
|
|
335
343
|
return :already_installed
|
|
336
344
|
end
|
|
337
345
|
|
|
338
|
-
game_files = glob_game_files(game).select { |file| file_belongs_to_game?(file, game,
|
|
346
|
+
game_files = glob_game_files(game).select { |file| file_belongs_to_game?(file, game, parsed_names) }.sort
|
|
339
347
|
if game_files.empty?
|
|
340
348
|
puts "#{shell.set_color("*", :red)} not found"
|
|
341
349
|
return :not_found
|
|
@@ -362,56 +370,29 @@ module Gamerom
|
|
|
362
370
|
.gsub('(', '*\(')
|
|
363
371
|
end
|
|
364
372
|
|
|
365
|
-
# A globbed file
|
|
366
|
-
#
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
return
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
# "Game.zip", "Game_Name.zip" or "Game (USA).zip"
|
|
379
|
-
def exact_filename_match?(filename, name)
|
|
380
|
-
[name, name.tr(' ', '_')].any? do |variant|
|
|
381
|
-
filename.match?(/\A#{Regexp.escape(variant)}(\s*\(\w+\))?\.\w+\z/i)
|
|
373
|
+
# A globbed file belongs to a game when, ignoring case and punctuation, the file name
|
|
374
|
+
# without its tags equals the game name without its tags, and every tag in the game
|
|
375
|
+
# name is present in the file. Among games with the same base name the one whose tags
|
|
376
|
+
# match the file best wins, so "Mega Man 2 (Capcom Town)" takes
|
|
377
|
+
# "Mega_Man_2_(USA)_(Capcom_Town).zip" and plain "Mega Man 2" leaves it alone.
|
|
378
|
+
def file_belongs_to_game?(file, game, parsed_names)
|
|
379
|
+
base, tags = parse_rom_name(File.basename(file).sub(/(\.[a-z0-9]{1,4})+\z/, '')) # strips ".zip", ".nkit.iso"
|
|
380
|
+
name_base, name_tags = parsed_names[game.id]
|
|
381
|
+
return false unless base == name_base && (name_tags - tags).empty?
|
|
382
|
+
|
|
383
|
+
parsed_names.none? do |other_id, (other_base, other_tags)|
|
|
384
|
+
other_id != game.id && other_base == base && (other_tags - tags).empty? && other_tags.size > name_tags.size
|
|
382
385
|
end
|
|
383
386
|
end
|
|
384
387
|
|
|
385
|
-
# The
|
|
386
|
-
def
|
|
387
|
-
|
|
388
|
-
|
|
388
|
+
# "Legend of Zelda, The: Ocarina of Time (USA) (GameCube)" => ["legendofzeldatheocarinaoftime", ["usa", "gamecube"]]
|
|
389
|
+
def parse_rom_name(str)
|
|
390
|
+
tags = str.scan(/[(\[]([^)\]]+)[)\]]/).flatten.map { |tag| normalize_rom_name(tag) }
|
|
391
|
+
[normalize_rom_name(str.gsub(/[(\[][^)\]]*[)\]]/, '')), tags]
|
|
389
392
|
end
|
|
390
393
|
|
|
391
|
-
def
|
|
392
|
-
|
|
393
|
-
game_names.any? do |other_id, other_name|
|
|
394
|
-
next false if other_id == game.id
|
|
395
|
-
|
|
396
|
-
other = other_name.downcase
|
|
397
|
-
next false unless other.length > name.length && other.start_with?(name)
|
|
398
|
-
|
|
399
|
-
filename_matches_other_game?(filename, other_name)
|
|
400
|
-
end
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
def filename_matches_other_game?(filename, other_name)
|
|
404
|
-
other = other_name.downcase
|
|
405
|
-
other_underscored = other.tr(' ', '_')
|
|
406
|
-
return false unless filename.include?(other_underscored) || filename.include?(other)
|
|
407
|
-
|
|
408
|
-
number = sequel_number(other_name)
|
|
409
|
-
return filename.include?(number) if number
|
|
410
|
-
|
|
411
|
-
filename.start_with?(other_underscored) ||
|
|
412
|
-
filename.start_with?(other) ||
|
|
413
|
-
filename.include?(other_underscored.tr(':', '_')) ||
|
|
414
|
-
filename.include?(other.tr(':', '-'))
|
|
394
|
+
def normalize_rom_name(str)
|
|
395
|
+
str.downcase.gsub(/[^a-z0-9]/, '')
|
|
415
396
|
end
|
|
416
397
|
|
|
417
398
|
def print_recovered_files(game_files)
|
data/lib/gamerom/repo.rb
CHANGED
|
@@ -77,8 +77,14 @@ module Gamerom
|
|
|
77
77
|
|
|
78
78
|
def update_database(platform)
|
|
79
79
|
games = @repo.games platform
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
database = "#{Gamerom::CACHE_DIR}/#{@name}/#{platform}.yml"
|
|
81
|
+
# An empty crawl means the site changed or blocked us; never wipe a good database over it.
|
|
82
|
+
if games.empty? && File.exist?(database) && !YAML.load_file(database).empty?
|
|
83
|
+
raise Gamerom::Error, "no games found for #{platform} on #{@name}, keeping the existing database"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
FileUtils.mkdir_p(File.dirname(database))
|
|
87
|
+
File.write(database, games.to_yaml)
|
|
82
88
|
end
|
|
83
89
|
end
|
|
84
90
|
end
|
data/lib/gamerom/repo_adapter.rb
CHANGED
|
@@ -2,12 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
require 'nokogiri'
|
|
4
4
|
require 'rest-client'
|
|
5
|
+
require 'uri'
|
|
5
6
|
|
|
6
7
|
module Gamerom
|
|
7
8
|
# RepoAdapter - Common adapter methods
|
|
8
9
|
module RepoAdapter
|
|
10
|
+
MAX_RETRIES = 5
|
|
11
|
+
|
|
12
|
+
# Sites rate limit the section crawl; honor the Retry-After of a 429 instead of failing.
|
|
9
13
|
def nokogiri_get(url)
|
|
10
|
-
|
|
14
|
+
retries = 0
|
|
15
|
+
loop do
|
|
16
|
+
return Nokogiri::HTML(RestClient.get(url))
|
|
17
|
+
rescue RestClient::TooManyRequests => e
|
|
18
|
+
retries += 1
|
|
19
|
+
raise if retries > MAX_RETRIES
|
|
20
|
+
|
|
21
|
+
delay = (e.response.headers[:retry_after] || 60).to_i
|
|
22
|
+
puts "\nrate limited by #{URI.parse(url).host}, retrying in #{delay}s (#{retries}/#{MAX_RETRIES})"
|
|
23
|
+
sleep delay
|
|
24
|
+
end
|
|
11
25
|
end
|
|
12
26
|
|
|
13
27
|
def games(platform)
|
|
@@ -105,10 +105,15 @@ module Gamerom
|
|
|
105
105
|
}
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
-
#
|
|
108
|
+
# One region per game from the No-Intro tags: "Espial (USA)" => "USA".
|
|
109
|
+
# Multi-region releases that include USA stay under USA ("Game (USA, Europe)" => "USA"),
|
|
110
|
+
# otherwise the alphabetically first region ("Game (Japan, Europe)" => "Europe").
|
|
109
111
|
def self.region(name)
|
|
110
|
-
groups = name.scan(/\(([^)]+)\)/).flatten
|
|
111
|
-
groups.find { |
|
|
112
|
+
groups = name.scan(/\(([^)]+)\)/).flatten.map { |group| group.split(',').map(&:strip) }
|
|
113
|
+
regions = groups.find { |tokens| REGIONS.include?(tokens.first) }
|
|
114
|
+
return 'Unknown' unless regions
|
|
115
|
+
|
|
116
|
+
regions.include?('USA') ? 'USA' : regions.min
|
|
112
117
|
end
|
|
113
118
|
|
|
114
119
|
def self.install(game)
|
|
@@ -52,21 +52,36 @@ module Gamerom
|
|
|
52
52
|
def self.extract_games(platform)
|
|
53
53
|
sections.each_with_index do |section, index|
|
|
54
54
|
page = nokogiri_get("https://vimm.net/vault/?p=list&system=#{platform}§ion=#{section}")
|
|
55
|
-
|
|
56
|
-
yield
|
|
55
|
+
rows = page.css('table.hovertable tr').select { |row| row.at_css(TITLE_LINK) }
|
|
56
|
+
yield rows.map { |row| game(row) }, index
|
|
57
57
|
rescue RestClient::NotFound
|
|
58
58
|
yield [], index
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
# Each row: <td><img class="media-icon"><a href="/vault/2640">Mace: The Dark Age</a></td>
|
|
63
|
+
# <td><img class="flag" title="USA"></td>...
|
|
64
|
+
TITLE_LINK = 'td:first-child a[href^="/vault/"]'
|
|
65
|
+
|
|
66
|
+
def self.game(row)
|
|
67
|
+
link = row.at_css(TITLE_LINK)
|
|
63
68
|
{
|
|
64
|
-
id:
|
|
65
|
-
name:
|
|
66
|
-
region:
|
|
69
|
+
id: link['href'].split('/').last.to_i,
|
|
70
|
+
name: link.text,
|
|
71
|
+
region: region(row),
|
|
67
72
|
}
|
|
68
73
|
end
|
|
69
74
|
|
|
75
|
+
# One region per game, taken from the flag icons. Multi-region releases that include
|
|
76
|
+
# USA stay under USA, so the vault keeps its US-centric layout; otherwise the
|
|
77
|
+
# alphabetically first flag.
|
|
78
|
+
def self.region(row)
|
|
79
|
+
flags = row.css('img.flag').map { |flag| flag['title'] }.compact
|
|
80
|
+
return 'USA' if flags.empty? || flags.include?('USA')
|
|
81
|
+
|
|
82
|
+
flags.min
|
|
83
|
+
end
|
|
84
|
+
|
|
70
85
|
# Vimm's Lair protects game pages with a browser challenge, so games have to be
|
|
71
86
|
# downloaded by hand. Prepare the target directory and tell the user what to do.
|
|
72
87
|
def self.install(game)
|
data/lib/gamerom/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gamerom
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lucas Mundim
|
|
@@ -139,7 +139,6 @@ licenses:
|
|
|
139
139
|
- MIT
|
|
140
140
|
metadata:
|
|
141
141
|
allowed_push_host: https://rubygems.org
|
|
142
|
-
homepage_uri: https://github.com/lucasmundim/gamerom
|
|
143
142
|
source_code_uri: https://github.com/lucasmundim/gamerom
|
|
144
143
|
changelog_uri: https://github.com/lucasmundim/gamerom/blob/master/CHANGELOG.md
|
|
145
144
|
rubygems_mfa_required: 'true'
|