gamerom 0.4.1 → 0.6.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/.github/workflows/main.yml +5 -7
- data/.rubocop.yml +9 -0
- data/.ruby-version +1 -1
- data/.tool-versions +1 -0
- data/CHANGELOG.md +22 -1
- data/Dockerfile +13 -3
- data/Gemfile +5 -3
- data/Gemfile.lock +94 -64
- data/Makefile +4 -4
- data/README.md +83 -50
- data/entrypoint.sh +1 -0
- data/exe/gamerom +16 -0
- data/gamerom.gemspec +8 -7
- data/lib/gamerom/cli.rb +126 -28
- data/lib/gamerom/errors.rb +9 -0
- data/lib/gamerom/game.rb +13 -3
- data/lib/gamerom/game_info.rb +1 -1
- data/lib/gamerom/repo.rb +2 -2
- data/lib/gamerom/repo_adapter.rb +14 -3
- data/lib/gamerom/repo_adapters/coolrom.rb +6 -2
- data/lib/gamerom/repo_adapters/edgeemu.rb +136 -0
- data/lib/gamerom/repo_adapters/vimm.rb +27 -36
- data/lib/gamerom/version.rb +1 -1
- data/lib/gamerom.rb +1 -4
- data/lib/mechanizeprogress.rb +37 -2
- metadata +16 -16
- data/lib/gamerom/repo_adapters/romnation.rb +0 -110
data/lib/gamerom/cli.rb
CHANGED
|
@@ -31,11 +31,11 @@ module Gamerom
|
|
|
31
31
|
validate_platform repo, options[:platform]
|
|
32
32
|
puts "showing info for game #{game_identifier} on #{options[:platform]} platform on #{options[:repo]} repo..."
|
|
33
33
|
game = repo.find(options[:platform], game_identifier)
|
|
34
|
-
if
|
|
34
|
+
if game.nil?
|
|
35
|
+
shell.say "Game #{game_identifier} not found", :red
|
|
36
|
+
else
|
|
35
37
|
puts game
|
|
36
38
|
puts game.filenames if game.installed?
|
|
37
|
-
else
|
|
38
|
-
shell.say "Game #{game_identifier} not found", :red
|
|
39
39
|
end
|
|
40
40
|
rescue StandardError => e
|
|
41
41
|
render_error e, options
|
|
@@ -61,6 +61,8 @@ module Gamerom
|
|
|
61
61
|
end
|
|
62
62
|
game.install
|
|
63
63
|
shell.say 'Game installed', :green
|
|
64
|
+
rescue Gamerom::ManualDownloadRequired => e
|
|
65
|
+
shell.say e.message, :yellow
|
|
64
66
|
rescue StandardError => e
|
|
65
67
|
render_error e, options
|
|
66
68
|
exit 1
|
|
@@ -75,7 +77,13 @@ module Gamerom
|
|
|
75
77
|
validate_platform repo, options[:platform]
|
|
76
78
|
games = repo.games options[:platform], region: options[:region]
|
|
77
79
|
games.each do |game|
|
|
78
|
-
|
|
80
|
+
if game.installed?
|
|
81
|
+
shell.say "Game #{game.name} already installed", :yellow
|
|
82
|
+
next
|
|
83
|
+
end
|
|
84
|
+
install(game.id)
|
|
85
|
+
rescue StandardError, SystemExit => e
|
|
86
|
+
shell.say "Error installing game #{game.name}: #{e.message}", :red
|
|
79
87
|
end
|
|
80
88
|
rescue StandardError => e
|
|
81
89
|
render_error e, options
|
|
@@ -116,28 +124,12 @@ module Gamerom
|
|
|
116
124
|
validate_platform repo, options[:platform]
|
|
117
125
|
puts "recovering state of roms for #{options[:platform]} platform on #{options[:repo]} repo..."
|
|
118
126
|
games = repo.games options[:platform]
|
|
119
|
-
|
|
120
|
-
games.
|
|
121
|
-
filename = nil
|
|
122
|
-
basename = "#{Gamerom::GAME_DIR}/#{repo.name}/#{options[:platform]}/#{game[:region]}/#{game[:name]}"
|
|
123
|
-
%w[zip 7z rar].each do |ext|
|
|
124
|
-
fullname = "#{basename}.#{ext}"
|
|
125
|
-
filename = fullname if File.exist? fullname
|
|
126
|
-
end
|
|
127
|
+
game_names = games.to_h { |game| [game.id, game.name] }
|
|
128
|
+
results = games.group_by { |game| recover_game(game, game_names) }
|
|
127
129
|
|
|
128
|
-
|
|
129
|
-
game.update_state File.basename(filename)
|
|
130
|
-
puts "Found game #{game[:name]}"
|
|
131
|
-
else
|
|
132
|
-
games_not_found << game[:name]
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
if games_not_found.count.positive?
|
|
136
|
-
puts 'Games not found:'
|
|
137
|
-
puts games_not_found
|
|
138
|
-
end
|
|
130
|
+
print_recover_summary(results.fetch(:found, []), results.fetch(:already_installed, []), results.fetch(:not_found, []))
|
|
139
131
|
rescue StandardError => e
|
|
140
|
-
|
|
132
|
+
render_error e, options
|
|
141
133
|
exit 1
|
|
142
134
|
end
|
|
143
135
|
|
|
@@ -308,10 +300,8 @@ module Gamerom
|
|
|
308
300
|
private
|
|
309
301
|
|
|
310
302
|
def print_game_table(games)
|
|
311
|
-
results =
|
|
312
|
-
|
|
313
|
-
games.each do |game|
|
|
314
|
-
results << [
|
|
303
|
+
results = games.map do |game|
|
|
304
|
+
[
|
|
315
305
|
game.id,
|
|
316
306
|
game.name,
|
|
317
307
|
game.region,
|
|
@@ -334,5 +324,113 @@ module Gamerom
|
|
|
334
324
|
|
|
335
325
|
raise "Expected '--platform' to be one of #{repo.platforms.keys.join(", ")}; got #{platform}"
|
|
336
326
|
end
|
|
327
|
+
|
|
328
|
+
# Recover helpers
|
|
329
|
+
|
|
330
|
+
def recover_game(game, game_names)
|
|
331
|
+
puts
|
|
332
|
+
puts "Checking game #{game}"
|
|
333
|
+
if game.filenames.any?
|
|
334
|
+
puts "#{shell.set_color("*", :green)} Found game at: #{game.filenames.first}"
|
|
335
|
+
return :already_installed
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
game_files = glob_game_files(game).select { |file| file_belongs_to_game?(file, game, game_names) }.sort
|
|
339
|
+
if game_files.empty?
|
|
340
|
+
puts "#{shell.set_color("*", :red)} not found"
|
|
341
|
+
return :not_found
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
game.update_state(game_files.map { |file| File.basename(file) })
|
|
345
|
+
print_recovered_files(game_files)
|
|
346
|
+
:found
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def glob_game_files(game)
|
|
350
|
+
patterns = [
|
|
351
|
+
glob_escape(game.name.tr(' ', '_'), '_'),
|
|
352
|
+
glob_escape(game.name, ' ')
|
|
353
|
+
]
|
|
354
|
+
patterns.flat_map { |pattern| Dir.glob("#{game.filepath}/#{pattern}*") }.uniq
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def glob_escape(str, spacer)
|
|
358
|
+
str
|
|
359
|
+
.gsub(/([\\\[\]*?{}) ,])/, '\\\\\1')
|
|
360
|
+
.gsub(':', "#{spacer}-")
|
|
361
|
+
.gsub('!', '\!*')
|
|
362
|
+
.gsub('(', '*\(')
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# A globbed file is kept for a game unless it is a better match for another
|
|
366
|
+
# game whose name starts with this game's name (e.g. "Kung-Fu" vs "Kung-Fu Master").
|
|
367
|
+
def file_belongs_to_game?(file, game, game_names)
|
|
368
|
+
filename = File.basename(file).downcase
|
|
369
|
+
return true if exact_filename_match?(filename, game.name.downcase)
|
|
370
|
+
|
|
371
|
+
number = sequel_number(game.name)
|
|
372
|
+
return true if number && filename.include?(number)
|
|
373
|
+
return true if game.name.include?('Limited Run Games') && filename.include?('limited_run')
|
|
374
|
+
|
|
375
|
+
!claimed_by_other_game?(filename, game, game_names)
|
|
376
|
+
end
|
|
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)
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# The number that identifies a sequel, e.g. "2" in "Tony Hawk's Pro Skater 2"
|
|
386
|
+
def sequel_number(name)
|
|
387
|
+
match = name.match(/(\d+|\s+\d+)[\s':]/) || name.match(/\s(\d+)$/)
|
|
388
|
+
match && match[1].strip
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def claimed_by_other_game?(filename, game, game_names)
|
|
392
|
+
name = game.name.downcase
|
|
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(':', '-'))
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def print_recovered_files(game_files)
|
|
418
|
+
if game_files.size > 1
|
|
419
|
+
puts "#{shell.set_color("*", :green)} Found #{game_files.size} disks for game:"
|
|
420
|
+
game_files.each { |file| puts " - #{file}" }
|
|
421
|
+
else
|
|
422
|
+
puts "#{shell.set_color("*", :green)} Found game at: #{game_files.first}"
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def print_recover_summary(games_found, games_already_installed, games_not_found)
|
|
427
|
+
if games_not_found.empty?
|
|
428
|
+
puts "All #{games_found.count + games_already_installed.count} games found"
|
|
429
|
+
else
|
|
430
|
+
puts "#{shell.set_color(games_found.count, :green)} games found"
|
|
431
|
+
puts "#{shell.set_color(games_already_installed.count, :yellow)} games already installed"
|
|
432
|
+
puts "#{shell.set_color(games_not_found.count, :red)} games not found"
|
|
433
|
+
end
|
|
434
|
+
end
|
|
337
435
|
end
|
|
338
436
|
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gamerom
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
# Raised by repo adapters whose site does not allow automated downloads.
|
|
7
|
+
# The message tells the user where to download the game and where to save it.
|
|
8
|
+
class ManualDownloadRequired < Error; end
|
|
9
|
+
end
|
data/lib/gamerom/game.rb
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
|
-
require 'ostruct'
|
|
5
4
|
require 'yaml'
|
|
6
5
|
|
|
7
6
|
module Gamerom
|
|
8
7
|
# Game - Represents a game ROM
|
|
9
|
-
class Game
|
|
8
|
+
class Game
|
|
9
|
+
attr_accessor :id, :name, :platform, :region, :repo, :tags, :url
|
|
10
|
+
|
|
11
|
+
def initialize(attributes = {})
|
|
12
|
+
@tags = []
|
|
13
|
+
attributes.each do |key, value|
|
|
14
|
+
instance_variable_set("@#{key}", value)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
10
18
|
def filenames
|
|
11
|
-
|
|
19
|
+
return [] unless File.exist? state_filename
|
|
20
|
+
|
|
21
|
+
Array(YAML.load_file(state_filename)).map do |filename|
|
|
12
22
|
"#{filepath}/#{filename}"
|
|
13
23
|
end
|
|
14
24
|
end
|
data/lib/gamerom/game_info.rb
CHANGED
|
@@ -84,7 +84,7 @@ module Gamerom
|
|
|
84
84
|
tags = []
|
|
85
85
|
codes = @name.scan(/\[(?<code>[^\]]+)\]/).flatten
|
|
86
86
|
codes.each do |code|
|
|
87
|
-
code = Regexp.last_match(1) if code.match
|
|
87
|
+
code = Regexp.last_match(1) if code.match(/^(?<code>[abcfhop])[0-9]*/)
|
|
88
88
|
tags << TAGS[code] if TAGS.include?(code)
|
|
89
89
|
end
|
|
90
90
|
tags
|
data/lib/gamerom/repo.rb
CHANGED
data/lib/gamerom/repo_adapter.rb
CHANGED
|
@@ -12,14 +12,25 @@ module Gamerom
|
|
|
12
12
|
|
|
13
13
|
def games(platform)
|
|
14
14
|
games = []
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
columns = `tput cols`.to_i
|
|
17
|
+
length = [120, columns].min
|
|
18
|
+
|
|
19
|
+
progress_bar = ProgressBar.create(
|
|
20
|
+
title: platform,
|
|
21
|
+
total: sections.count,
|
|
22
|
+
autofinish: true,
|
|
23
|
+
length: length,
|
|
24
|
+
format: "%t: %b\e[0;93m\u{15E7}\e[0m%i %j%% %e",
|
|
25
|
+
progress_mark: ' ',
|
|
26
|
+
remainder_mark: "\e[0;34m\u{FF65}\e[0m"
|
|
27
|
+
)
|
|
16
28
|
|
|
17
29
|
extract_games(platform) do |section_games, section_index|
|
|
18
30
|
games.append(*section_games)
|
|
19
|
-
progress_bar.
|
|
31
|
+
progress_bar.progress = section_index + 1
|
|
20
32
|
end
|
|
21
33
|
|
|
22
|
-
progress_bar.finish
|
|
23
34
|
games
|
|
24
35
|
end
|
|
25
36
|
end
|
|
@@ -33,6 +33,7 @@ module Gamerom
|
|
|
33
33
|
'saturn' => 'Sega Saturn',
|
|
34
34
|
'psx' => 'Sony Playstation',
|
|
35
35
|
'ps2' => 'Sony Playstation 2',
|
|
36
|
+
'ps3' => 'Sony Playstation 3',
|
|
36
37
|
'psp' => 'Sony Playstation Portable',
|
|
37
38
|
}.freeze
|
|
38
39
|
|
|
@@ -48,10 +49,12 @@ module Gamerom
|
|
|
48
49
|
sections.each_with_index do |section, index|
|
|
49
50
|
page = nokogiri_get("https://coolrom.com.au/roms/#{platform}/#{section}/")
|
|
50
51
|
regions = page.css('input.region').map { |i| i['name'] }
|
|
52
|
+
game_links = []
|
|
51
53
|
regions.each do |region|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
region_game_links = page.css("div.#{region} a")
|
|
55
|
+
game_links.append(*region_game_links.map { |game_link| game(game_link, region) })
|
|
54
56
|
end
|
|
57
|
+
yield game_links, index
|
|
55
58
|
end
|
|
56
59
|
end
|
|
57
60
|
|
|
@@ -78,6 +81,7 @@ module Gamerom
|
|
|
78
81
|
filename = response.filename
|
|
79
82
|
FileUtils.mkdir_p(game.filepath)
|
|
80
83
|
response.save!("#{game.filepath}/#{filename}")
|
|
84
|
+
puts "Downloaded: #{filename}"
|
|
81
85
|
yield [filename]
|
|
82
86
|
end
|
|
83
87
|
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mechanize'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'mechanize/progressbar'
|
|
6
|
+
require 'mechanizeprogress'
|
|
7
|
+
|
|
8
|
+
module Gamerom
|
|
9
|
+
module RepoAdapters
|
|
10
|
+
# Edgeemu - An adapter for the Edge Emulation repository website
|
|
11
|
+
class Edgeemu
|
|
12
|
+
extend Gamerom::RepoAdapter
|
|
13
|
+
|
|
14
|
+
BASE_URL = 'https://edgeemu.net'
|
|
15
|
+
|
|
16
|
+
PLATFORM = {
|
|
17
|
+
'atari-2600' => 'Atari 2600',
|
|
18
|
+
'atari-5200' => 'Atari 5200',
|
|
19
|
+
'atari-7800' => 'Atari 7800',
|
|
20
|
+
'atari-jaguar' => 'Atari Jaguar',
|
|
21
|
+
'atari-jaguar-cd' => 'Atari Jaguar CD',
|
|
22
|
+
'atari-lynx' => 'Atari Lynx',
|
|
23
|
+
'atari-st' => 'Atari ST',
|
|
24
|
+
'bandai-wonderswan' => 'Bandai WonderSwan',
|
|
25
|
+
'colecovision' => 'Coleco ColecoVision',
|
|
26
|
+
'commodore-64' => 'Commodore 64',
|
|
27
|
+
'commodore-amiga' => 'Commodore Amiga',
|
|
28
|
+
'commodore-amiga-cd' => 'Commodore Amiga CD',
|
|
29
|
+
'commodore-amiga-cd32' => 'Commodore Amiga CD32',
|
|
30
|
+
'commodore-plus-4' => 'Commodore Plus-4',
|
|
31
|
+
'commodore-vic-20' => 'Commodore VIC-20',
|
|
32
|
+
'fairchild-channel-f' => 'Fairchild Channel F',
|
|
33
|
+
'gce-vectrex' => 'GCE Vectrex',
|
|
34
|
+
'microsoft-msx' => 'Microsoft MSX / MSX2',
|
|
35
|
+
'nec-pc-engine-cd-turbografx-cd' => 'NEC PC Engine CD / TurboGrafx CD',
|
|
36
|
+
'nec-pc-engine-supergrafx' => 'NEC PC Engine SuperGrafx',
|
|
37
|
+
'nec-pc-engine-turbografx-16' => 'NEC PC Engine / TurboGrafx 16',
|
|
38
|
+
'nintendo-64' => 'Nintendo 64',
|
|
39
|
+
'nintendo-ds' => 'Nintendo DS',
|
|
40
|
+
'nintendo-fds' => 'Nintendo Famicom Disk System (FDS)',
|
|
41
|
+
'nintendo-gameboy' => 'Nintendo Game Boy',
|
|
42
|
+
'nintendo-gameboy-advance' => 'Nintendo Game Boy Advance',
|
|
43
|
+
'nintendo-gameboy-color' => 'Nintendo Game Boy Color',
|
|
44
|
+
'nintendo-gamecube' => 'Nintendo Gamecube',
|
|
45
|
+
'nintendo-nes' => 'Nintendo Entertainment System (NES)',
|
|
46
|
+
'nintendo-satellaview' => 'Nintendo Satellaview',
|
|
47
|
+
'nintendo-snes' => 'Super NES (SNES)',
|
|
48
|
+
'nintendo-virtualboy' => 'Nintendo Virtual Boy',
|
|
49
|
+
'panasonic-3do' => 'Panasonic 3DO',
|
|
50
|
+
'philips-cdi' => 'Philips CDi',
|
|
51
|
+
'rca-studioii' => 'RCA Studio II',
|
|
52
|
+
'sega-32x' => 'Sega 32X',
|
|
53
|
+
'sega-cd' => 'Sega Mega-CD / Sega CD',
|
|
54
|
+
'sega-dreamcast' => 'Sega Dreamcast',
|
|
55
|
+
'sega-gamegear' => 'Sega Game Gear',
|
|
56
|
+
'sega-genesis' => 'Sega Mega Drive / Genesis',
|
|
57
|
+
'sega-pico' => 'Sega PICO',
|
|
58
|
+
'sega-saturn' => 'Sega Saturn',
|
|
59
|
+
'sega-sg1000' => 'Sega SG-1000',
|
|
60
|
+
'sega-sms' => 'Sega Master System / Mark III',
|
|
61
|
+
'sinclair-zx-spectrum-3' => 'Sinclair ZX Spectrum +3',
|
|
62
|
+
'snk-neo-geo-cd' => 'SNK Neo Geo CD',
|
|
63
|
+
'snk-ngpc' => 'SNK Neo Geo Pocket Color',
|
|
64
|
+
'watara-supervision' => 'Watara Supervision',
|
|
65
|
+
}.freeze
|
|
66
|
+
|
|
67
|
+
# No-Intro region tags, as they appear in parentheses in the file names
|
|
68
|
+
REGIONS = [
|
|
69
|
+
'World', 'USA', 'Europe', 'Japan', 'Asia', 'Australia', 'Brazil', 'Canada', 'China', 'France',
|
|
70
|
+
'Germany', 'Greece', 'Hong Kong', 'Italy', 'Korea', 'Netherlands', 'Norway', 'Poland', 'Portugal',
|
|
71
|
+
'Russia', 'Scandinavia', 'Spain', 'Sweden', 'Taiwan', 'UK', 'Unknown', 'Latin America'
|
|
72
|
+
].freeze
|
|
73
|
+
|
|
74
|
+
def self.platforms
|
|
75
|
+
PLATFORM
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.sections
|
|
79
|
+
('a'..'z').to_a.unshift('1')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.extract_games(platform)
|
|
83
|
+
sections.each_with_index do |section, index|
|
|
84
|
+
page = nokogiri_get("#{BASE_URL}/browse/#{platform}/#{section}")
|
|
85
|
+
yield page.css('div.item details').map { |item| game(item) }, index
|
|
86
|
+
rescue RestClient::NotFound
|
|
87
|
+
yield [], index
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Each item looks like:
|
|
92
|
+
# <details data-name="Espial (USA).zip">
|
|
93
|
+
# <summary>Espial (USA)</summary>
|
|
94
|
+
# <p><a href="/download/atari-2600/Espial%20%28USA%29.zip">download</a> ...</p>
|
|
95
|
+
# <p>hash: <span>931A0BDC</span></p>
|
|
96
|
+
# </details>
|
|
97
|
+
# The hash is the CRC32 of the ROM, which is used as the game id.
|
|
98
|
+
def self.game(item)
|
|
99
|
+
name = item.at_css('summary').text.strip
|
|
100
|
+
{
|
|
101
|
+
id: item.css('p').find { |p| p.text.start_with?('hash') }.at_css('span').text.strip.to_i(16),
|
|
102
|
+
name: name,
|
|
103
|
+
region: region(name),
|
|
104
|
+
url: "#{BASE_URL}#{item.at_css('a[href^="/download/"]')["href"]}",
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# "Espial (USA)" => "USA", "Game (USA, Europe) (Proto)" => "USA, Europe"
|
|
109
|
+
def self.region(name)
|
|
110
|
+
groups = name.scan(/\(([^)]+)\)/).flatten
|
|
111
|
+
groups.find { |group| REGIONS.include?(group.split(',').first.strip) } || 'Unknown'
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.install(game)
|
|
115
|
+
agent = Mechanize.new
|
|
116
|
+
agent.pluggable_parser.default = Mechanize::Download
|
|
117
|
+
agent.user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36'
|
|
118
|
+
|
|
119
|
+
response = nil
|
|
120
|
+
agent.progressbar do
|
|
121
|
+
response = agent.get(game.url)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
return unless response.code.to_i == 200
|
|
125
|
+
|
|
126
|
+
# The Content-Disposition header ends with a stray ";" that Mechanize cannot parse,
|
|
127
|
+
# so it falls back to the percent-encoded URL basename. Decode it instead.
|
|
128
|
+
filename = URI.decode_uri_component(File.basename(URI.parse(game.url).path))
|
|
129
|
+
FileUtils.mkdir_p(game.filepath)
|
|
130
|
+
response.save!("#{game.filepath}/#{filename}")
|
|
131
|
+
puts "Downloaded: #{filename}"
|
|
132
|
+
yield [filename]
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
4
|
-
require 'mechanize/progressbar'
|
|
5
|
-
require 'mechanizeprogress'
|
|
3
|
+
require 'fileutils'
|
|
6
4
|
|
|
7
5
|
module Gamerom
|
|
8
6
|
module RepoAdapters
|
|
@@ -11,6 +9,10 @@ module Gamerom
|
|
|
11
9
|
extend Gamerom::RepoAdapter
|
|
12
10
|
|
|
13
11
|
PLATFORM = {
|
|
12
|
+
'32X' => 'Sega 32X',
|
|
13
|
+
'Atari2600' => 'Atari 2600',
|
|
14
|
+
'Atari5200' => 'Atari 5200',
|
|
15
|
+
'Atari7800' => 'Atari 7800',
|
|
14
16
|
'Dreamcast' => 'Dreamcast',
|
|
15
17
|
'DS' => 'Nintendo DS',
|
|
16
18
|
'GameCube' => 'GameCube',
|
|
@@ -18,6 +20,8 @@ module Gamerom
|
|
|
18
20
|
'GBA' => 'Game Boy Advance',
|
|
19
21
|
'GBC' => 'Game Boy Color',
|
|
20
22
|
'Genesis' => 'Genesis',
|
|
23
|
+
'GG' => 'Game Gear',
|
|
24
|
+
'Lynx' => 'Lynx',
|
|
21
25
|
'N64' => 'Nintendo 64',
|
|
22
26
|
'NES' => 'Nintendo',
|
|
23
27
|
'PS1' => 'PlayStation',
|
|
@@ -25,9 +29,16 @@ module Gamerom
|
|
|
25
29
|
'PS3' => 'PlayStation 3',
|
|
26
30
|
'PSP' => 'PlayStation Portable',
|
|
27
31
|
'Saturn' => 'Saturn',
|
|
32
|
+
'SegaCD' => 'Sega CD',
|
|
33
|
+
'SMS' => 'Master System',
|
|
28
34
|
'SNES' => 'Super Nintendo',
|
|
35
|
+
'TG16' => 'TurboGrafx-16',
|
|
36
|
+
'TGCD' => 'TurboGrafx-CD',
|
|
37
|
+
'VB' => 'Virtual Boy',
|
|
29
38
|
'Wii' => 'Wii',
|
|
30
39
|
'WiiWare' => 'WiiWare',
|
|
40
|
+
'Xbox' => 'Xbox',
|
|
41
|
+
'Xbox360' => 'Xbox 360',
|
|
31
42
|
}.freeze
|
|
32
43
|
|
|
33
44
|
def self.platforms
|
|
@@ -43,6 +54,8 @@ module Gamerom
|
|
|
43
54
|
page = nokogiri_get("https://vimm.net/vault/?p=list&system=#{platform}§ion=#{section}")
|
|
44
55
|
game_links = page.css('table.hovertable td:first-child a:first-child')
|
|
45
56
|
yield game_links.map { |game_link| game(game_link) }, index
|
|
57
|
+
rescue RestClient::NotFound
|
|
58
|
+
yield [], index
|
|
46
59
|
end
|
|
47
60
|
end
|
|
48
61
|
|
|
@@ -54,42 +67,20 @@ module Gamerom
|
|
|
54
67
|
}
|
|
55
68
|
end
|
|
56
69
|
|
|
70
|
+
# Vimm's Lair protects game pages with a browser challenge, so games have to be
|
|
71
|
+
# downloaded by hand. Prepare the target directory and tell the user what to do.
|
|
57
72
|
def self.install(game)
|
|
58
73
|
FileUtils.mkdir_p(game.filepath)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
game_files = []
|
|
67
|
-
multiple_disks = page.css('#download_disc_number')
|
|
68
|
-
|
|
69
|
-
if multiple_disks.empty?
|
|
70
|
-
game_files << { id: form['mediaId'], name: 'single file rom' }
|
|
71
|
-
else
|
|
72
|
-
puts 'multiple discs detected'
|
|
73
|
-
game_files.concat(multiple_disks.children[1..-2].map { |disk| { name: disk.text, id: disk['value'] } })
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
game_files.each do |game_file|
|
|
77
|
-
puts "downloading #{game_file[:name]}"
|
|
78
|
-
form.method = 'GET'
|
|
79
|
-
button = form.button_with(type: 'submit')
|
|
80
|
-
response = nil
|
|
81
|
-
form['mediaId'] = game_file[:id]
|
|
82
|
-
agent.progressbar do
|
|
83
|
-
response = form.click_button(button)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
break unless response.code.to_i == 200
|
|
74
|
+
raise Gamerom::ManualDownloadRequired, <<~MSG
|
|
75
|
+
vimm.net does not allow automated downloads.
|
|
76
|
+
Download the game from #{game_url(game)} in your browser and save it to:
|
|
77
|
+
#{game.filepath}/
|
|
78
|
+
Then run `gamerom recover -r vimm -p #{game.platform}` to register it.
|
|
79
|
+
MSG
|
|
80
|
+
end
|
|
87
81
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
filenames << filename
|
|
91
|
-
end
|
|
92
|
-
yield filenames
|
|
82
|
+
def self.game_url(game)
|
|
83
|
+
"https://vimm.net/vault/#{game.id}"
|
|
93
84
|
end
|
|
94
85
|
end
|
|
95
86
|
end
|
data/lib/gamerom/version.rb
CHANGED
data/lib/gamerom.rb
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'gamerom/errors'
|
|
3
4
|
require_relative 'gamerom/config'
|
|
4
5
|
require_relative 'gamerom/game'
|
|
5
6
|
require_relative 'gamerom/game_info'
|
|
6
7
|
require_relative 'gamerom/repo'
|
|
7
8
|
require_relative 'gamerom/cli'
|
|
8
9
|
require_relative 'gamerom/version'
|
|
9
|
-
|
|
10
|
-
module Gamerom
|
|
11
|
-
class Error < StandardError; end
|
|
12
|
-
end
|
data/lib/mechanizeprogress.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# Mechanize - A Monkeypatch for the mechanize progressbar
|
|
4
4
|
class Mechanize
|
|
5
5
|
include MechanizeProgressBarAPI # (1of7)a
|
|
6
|
+
|
|
6
7
|
class HTTP
|
|
7
8
|
# Agent - A Monkeypatch for the mechanize progressbar
|
|
8
9
|
class Agent
|
|
@@ -31,11 +32,11 @@ class Mechanize
|
|
|
31
32
|
log.debug("Read #{total} bytes total") if log && !mpbar.suppress_logger? # (7of7)a
|
|
32
33
|
|
|
33
34
|
raise Mechanize::ResponseCodeError, response if
|
|
34
|
-
Net::HTTPUnknownResponse
|
|
35
|
+
response == Net::HTTPUnknownResponse
|
|
35
36
|
|
|
36
37
|
content_length = response.content_length
|
|
37
38
|
|
|
38
|
-
unless Net::HTTP::Head
|
|
39
|
+
unless request == Net::HTTP::Head || response == Net::HTTPRedirection
|
|
39
40
|
if content_length && content_length != body_io.length
|
|
40
41
|
raise EOFError, "Content-Length (#{content_length}) does not match response body length (#{body_io.length})"
|
|
41
42
|
end
|
|
@@ -46,3 +47,37 @@ class Mechanize
|
|
|
46
47
|
end
|
|
47
48
|
end
|
|
48
49
|
end
|
|
50
|
+
|
|
51
|
+
# MechanizeProgressBar - A Monkeypatch for the mechanize progressbar
|
|
52
|
+
class MechanizeProgressBar
|
|
53
|
+
def inc(step)
|
|
54
|
+
@progressbar.progress = @progressbar.progress + step if @progressbar
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def progressbar_new(pbar_opts, request, response)
|
|
58
|
+
out = pbar_opts[:out] || pbar_opts[:output] || $stderr
|
|
59
|
+
format = pbar_opts[:format] || "%j%% %b\e[0;93m\u{15E7}\e[0m%i Progress: %c/%C %a %e Speed: %rKB/sec %t"
|
|
60
|
+
if pbar_opts[:single]
|
|
61
|
+
title = pbar_opts[:title] || request['Host']
|
|
62
|
+
else
|
|
63
|
+
title = pbar_opts[:title] || ''
|
|
64
|
+
out.print "#{pbar_opts[:title] || uri(request)}\n"
|
|
65
|
+
end
|
|
66
|
+
total = pbar_opts[:total] || filesize(response)
|
|
67
|
+
pbar_class = pbar_opts[:reversed] ? ReversedProgressBar : ProgressBar
|
|
68
|
+
|
|
69
|
+
columns = `tput cols`.to_i
|
|
70
|
+
length = [120, columns].min
|
|
71
|
+
|
|
72
|
+
pbar_class.create(
|
|
73
|
+
title: title,
|
|
74
|
+
total: total,
|
|
75
|
+
output: out,
|
|
76
|
+
length: length,
|
|
77
|
+
format: format,
|
|
78
|
+
rate_scale: ->(rate) { rate / 1024 },
|
|
79
|
+
progress_mark: ' ',
|
|
80
|
+
remainder_mark: "\e[0;34m\u{FF65}\e[0m"
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|