gamerom 0.2.0 → 0.4.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 +4 -4
- data/.github/workflows/main.yml +1 -1
- data/.rubocop.yml +35 -2
- data/.ruby-version +1 -1
- data/CHANGELOG.md +27 -1
- data/Dockerfile +7 -2
- data/Gemfile +4 -4
- data/Gemfile.lock +5 -5
- data/Makefile +1 -1
- data/README.md +117 -71
- data/Rakefile +3 -3
- data/bin/console +3 -3
- data/entrypoint.sh +6 -0
- data/exe/gamerom +2 -1
- data/gamerom.gemspec +22 -22
- data/lib/gamerom.rb +6 -5
- data/lib/gamerom/cli.rb +93 -77
- data/lib/gamerom/config.rb +2 -2
- data/lib/gamerom/game.rb +20 -14
- data/lib/gamerom/game_info.rb +93 -0
- data/lib/gamerom/repo.rb +24 -25
- data/lib/gamerom/repo_adapter.rb +26 -0
- data/lib/gamerom/repo_adapters/coolrom.rb +29 -25
- data/lib/gamerom/repo_adapters/romnation.rb +110 -0
- data/lib/gamerom/repo_adapters/vimm.rb +33 -29
- data/lib/gamerom/version.rb +1 -1
- data/lib/mechanizeprogress.rb +14 -10
- metadata +8 -4
data/lib/gamerom/config.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'fileutils'
|
4
4
|
require 'logger'
|
@@ -6,7 +6,7 @@ require 'rest-client'
|
|
6
6
|
require 'mechanize'
|
7
7
|
|
8
8
|
module Gamerom
|
9
|
-
ROM_ROOT = ENV['ROM_ROOT'] || File.expand_path(
|
9
|
+
ROM_ROOT = ENV['ROM_ROOT'] || File.expand_path('~/.gamerom')
|
10
10
|
CACHE_DIR = ENV['CACHE_DIR'] || "#{ROM_ROOT}/cache"
|
11
11
|
GAME_DIR = ENV['GAME_DIR'] || "#{ROM_ROOT}/games"
|
12
12
|
LOG_DIR = ENV['LOG_DIR'] || "#{ROM_ROOT}/logs"
|
data/lib/gamerom/game.rb
CHANGED
@@ -1,50 +1,56 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'fileutils'
|
4
4
|
require 'ostruct'
|
5
5
|
require 'yaml'
|
6
6
|
|
7
7
|
module Gamerom
|
8
|
+
# Game - Represents a game ROM
|
8
9
|
class Game < OpenStruct
|
9
10
|
def filenames
|
10
|
-
YAML.load_file(
|
11
|
-
"#{
|
11
|
+
YAML.load_file(state_filename).map do |filename|
|
12
|
+
"#{filepath}/#{filename}"
|
12
13
|
end
|
13
14
|
end
|
14
15
|
|
15
16
|
def filepath
|
16
|
-
"#{Gamerom::GAME_DIR}/#{
|
17
|
+
"#{Gamerom::GAME_DIR}/#{repo.name}/#{platform}/#{region}"
|
17
18
|
end
|
18
19
|
|
19
20
|
def install
|
20
|
-
|
21
|
-
|
21
|
+
repo.install self do |filenames|
|
22
|
+
update_state filenames
|
22
23
|
end
|
23
24
|
end
|
24
25
|
|
25
26
|
def installed?
|
26
|
-
File.
|
27
|
+
File.exist? state_filename
|
27
28
|
end
|
28
29
|
|
29
30
|
def state_filename
|
30
|
-
"#{Gamerom::STATE_DIR}/#{
|
31
|
+
"#{Gamerom::STATE_DIR}/#{repo.name}/#{platform}/#{region}/#{id}"
|
31
32
|
end
|
32
33
|
|
33
34
|
def to_s
|
34
|
-
|
35
|
+
install_status = ''
|
36
|
+
install_status = " (#{shell.set_color "installed", :green})" if installed?
|
37
|
+
tags = ''
|
38
|
+
tags = " - tags: #{tags.join(", ")}" if respond_to?(:tags) && !tags.empty?
|
39
|
+
"#{id} - #{name} - #{region}#{install_status}#{tags}"
|
35
40
|
end
|
36
41
|
|
37
42
|
def uninstall
|
38
|
-
FileUtils.rm_rf
|
39
|
-
FileUtils.rm_rf
|
43
|
+
FileUtils.rm_rf filenames
|
44
|
+
FileUtils.rm_rf state_filename
|
40
45
|
end
|
41
46
|
|
42
|
-
def update_state
|
43
|
-
FileUtils.mkdir_p("#{Gamerom::STATE_DIR}/#{
|
44
|
-
File.write(
|
47
|
+
def update_state(filenames)
|
48
|
+
FileUtils.mkdir_p("#{Gamerom::STATE_DIR}/#{repo.name}/#{platform}/#{region}")
|
49
|
+
File.write(state_filename, filenames.to_yaml)
|
45
50
|
end
|
46
51
|
|
47
52
|
private
|
53
|
+
|
48
54
|
def shell
|
49
55
|
@shell ||= Thor::Shell::Color.new
|
50
56
|
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamerom
|
4
|
+
# GameInfo - Extracts region and tags from game name
|
5
|
+
class GameInfo
|
6
|
+
REGIONS = {
|
7
|
+
'1' => 'Japan & Korea',
|
8
|
+
'4' => 'USA & Brazil - NTSC',
|
9
|
+
'5' => 'NTSC',
|
10
|
+
'8' => 'PAL',
|
11
|
+
'A' => 'Australia',
|
12
|
+
'As' => 'Asia',
|
13
|
+
'B' => 'Brazil',
|
14
|
+
'C' => 'Canada',
|
15
|
+
'Ch' => 'China',
|
16
|
+
'D' => 'Netherlands (Dutch)',
|
17
|
+
'E' => 'Europe',
|
18
|
+
'F' => 'France',
|
19
|
+
'FC' => 'French Canadian',
|
20
|
+
'FN' => 'Finland',
|
21
|
+
'G' => 'Germany',
|
22
|
+
'GR' => 'Greece',
|
23
|
+
'H' => 'Holland',
|
24
|
+
'HK' => 'Hong Kong',
|
25
|
+
'I' => 'Italy',
|
26
|
+
'J' => 'Japan',
|
27
|
+
'JUE' => 'Japan & USA & Europe',
|
28
|
+
'K' => 'Korea',
|
29
|
+
'Nl' => 'Netherlands',
|
30
|
+
'NL' => 'Netherlands',
|
31
|
+
'No' => 'Norway',
|
32
|
+
'PD' => 'Public Domain',
|
33
|
+
'R' => 'Russia',
|
34
|
+
'S' => 'Spain',
|
35
|
+
'Sw' => 'Sweden',
|
36
|
+
'SW' => 'Sweden',
|
37
|
+
'U' => 'USA',
|
38
|
+
'UK' => 'England',
|
39
|
+
'Unk' => 'Unknown Country',
|
40
|
+
'Unl' => 'Unlicensed',
|
41
|
+
'PAL' => 'PAL regions (Australia, Europe)',
|
42
|
+
'NTSC' => 'NTSC regions (Japan, USA, Latin America)',
|
43
|
+
}.freeze
|
44
|
+
|
45
|
+
TAGS = {
|
46
|
+
'!' => :good,
|
47
|
+
'!p' => :pending,
|
48
|
+
'a' => :alternate,
|
49
|
+
'b' => :bad,
|
50
|
+
'BF' => :bung,
|
51
|
+
'c' => :checksum,
|
52
|
+
'C' => :color,
|
53
|
+
'f' => :fixed,
|
54
|
+
'h' => :hack,
|
55
|
+
'J' => :japanese_translation,
|
56
|
+
'o' => :overdump,
|
57
|
+
'p' => :pirate,
|
58
|
+
'PC10' => :pc10,
|
59
|
+
'S' => :super,
|
60
|
+
'T-' => :old_translation,
|
61
|
+
't' => :trained,
|
62
|
+
'T+' => :newer_translation,
|
63
|
+
'VS' => :vs,
|
64
|
+
'x' => :bad_checksum,
|
65
|
+
}.freeze
|
66
|
+
|
67
|
+
attr_reader :name
|
68
|
+
|
69
|
+
def initialize(name)
|
70
|
+
@name = name
|
71
|
+
end
|
72
|
+
|
73
|
+
def region
|
74
|
+
identifiers = @name.scan(/\((?<region>[A-Za-z0-9]+)\)/).flatten
|
75
|
+
region_id = identifiers.find { |i| REGIONS.include? i }
|
76
|
+
if region_id
|
77
|
+
REGIONS[region_id]
|
78
|
+
else
|
79
|
+
'USA'
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def tags
|
84
|
+
tags = []
|
85
|
+
codes = @name.scan(/\[(?<code>[^\]]+)\]/).flatten
|
86
|
+
codes.each do |code|
|
87
|
+
code = Regexp.last_match(1) if code.match /^(?<code>[abcfhop])[0-9]*/
|
88
|
+
tags << TAGS[code] if TAGS.include?(code)
|
89
|
+
end
|
90
|
+
tags
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/gamerom/repo.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'yaml'
|
4
4
|
|
@@ -6,30 +6,34 @@ REPOSITORIES = Dir["#{File.dirname(__FILE__)}/repo_adapters/*"].map do |file|
|
|
6
6
|
File.basename(file, '.rb')
|
7
7
|
end
|
8
8
|
|
9
|
+
require_relative 'repo_adapter'
|
9
10
|
REPOSITORIES.each do |repo|
|
10
11
|
require_relative "repo_adapters/#{repo}"
|
11
12
|
end
|
12
13
|
|
13
14
|
module Gamerom
|
15
|
+
# Repo - Represents a game ROM repository
|
14
16
|
class Repo
|
17
|
+
attr_reader :name
|
18
|
+
|
15
19
|
def self.list
|
16
20
|
REPOSITORIES.map do |repo|
|
17
|
-
|
21
|
+
new repo
|
18
22
|
end
|
19
23
|
end
|
20
24
|
|
21
|
-
def initialize
|
25
|
+
def initialize(name)
|
22
26
|
@name = name
|
23
27
|
@repo = Gamerom::RepoAdapters.const_get(name.capitalize)
|
24
28
|
end
|
25
29
|
|
26
|
-
def install
|
30
|
+
def install(game, &block)
|
27
31
|
@repo.install game, &block
|
28
32
|
end
|
29
33
|
|
30
|
-
def find
|
34
|
+
def find(platform, game_identifier)
|
31
35
|
games(platform).find do |game|
|
32
|
-
if
|
36
|
+
if Integer(game_identifier, exception: false)
|
33
37
|
game.id == game_identifier.to_i
|
34
38
|
else
|
35
39
|
game.name.downcase == game_identifier.downcase
|
@@ -37,47 +41,42 @@ module Gamerom
|
|
37
41
|
end
|
38
42
|
end
|
39
43
|
|
40
|
-
def games
|
44
|
+
def games(platform, options = {})
|
41
45
|
platform_database = "#{Gamerom::CACHE_DIR}/#{@name}/#{platform}.yml"
|
42
|
-
update_database platform unless File.
|
43
|
-
games = YAML.load_file(platform_database).map
|
46
|
+
update_database platform unless File.exist? platform_database
|
47
|
+
games = YAML.load_file(platform_database).map do |game|
|
44
48
|
Game.new(game.merge(platform: platform, repo: self))
|
45
|
-
|
49
|
+
end
|
46
50
|
|
47
|
-
|
48
|
-
games
|
49
|
-
|
50
|
-
|
51
|
+
unless options[:region].nil?
|
52
|
+
games.select! do |game|
|
53
|
+
game.region == options[:region]
|
54
|
+
end
|
51
55
|
end
|
52
56
|
|
53
|
-
|
54
|
-
games
|
57
|
+
unless options[:keyword].nil?
|
58
|
+
games.select! do |game|
|
55
59
|
game.name =~ /#{options[:keyword]}/i
|
56
|
-
|
60
|
+
end
|
57
61
|
end
|
58
62
|
|
59
63
|
games
|
60
64
|
end
|
61
65
|
|
62
|
-
def name
|
63
|
-
@name
|
64
|
-
end
|
65
|
-
|
66
66
|
def platforms
|
67
67
|
@repo.platforms
|
68
68
|
end
|
69
69
|
|
70
|
-
def regions
|
71
|
-
games(platform).map
|
70
|
+
def regions(platform)
|
71
|
+
games(platform).map(&:region).sort.uniq
|
72
72
|
end
|
73
73
|
|
74
74
|
def to_s
|
75
75
|
@name
|
76
76
|
end
|
77
77
|
|
78
|
-
def update_database
|
78
|
+
def update_database(platform)
|
79
79
|
games = @repo.games platform
|
80
|
-
puts
|
81
80
|
FileUtils.mkdir_p("#{Gamerom::CACHE_DIR}/#{@name}")
|
82
81
|
File.write("#{Gamerom::CACHE_DIR}/#{@name}/#{platform}.yml", games.to_yaml)
|
83
82
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'rest-client'
|
5
|
+
|
6
|
+
module Gamerom
|
7
|
+
# RepoAdapter - Common adapter methods
|
8
|
+
module RepoAdapter
|
9
|
+
def nokogiri_get(url)
|
10
|
+
Nokogiri::HTML(RestClient.get(url))
|
11
|
+
end
|
12
|
+
|
13
|
+
def games(platform)
|
14
|
+
games = []
|
15
|
+
progress_bar = ProgressBar.new(platform, sections.count)
|
16
|
+
|
17
|
+
extract_games(platform) do |section_games, section_index|
|
18
|
+
games.append(*section_games)
|
19
|
+
progress_bar.set(section_index + 1)
|
20
|
+
end
|
21
|
+
|
22
|
+
progress_bar.finish
|
23
|
+
games
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -3,12 +3,13 @@
|
|
3
3
|
require 'mechanize'
|
4
4
|
require 'mechanize/progressbar'
|
5
5
|
require 'mechanizeprogress'
|
6
|
-
require 'nokogiri'
|
7
|
-
require 'rest-client'
|
8
6
|
|
9
7
|
module Gamerom
|
10
8
|
module RepoAdapters
|
9
|
+
# Coolrom - An adapter for the CoolROM repository website
|
11
10
|
class Coolrom
|
11
|
+
extend Gamerom::RepoAdapter
|
12
|
+
|
12
13
|
PLATFORM = {
|
13
14
|
'atari2600' => 'Atari 2600',
|
14
15
|
'atari5200' => 'Atari 5200',
|
@@ -33,31 +34,33 @@ module Gamerom
|
|
33
34
|
'psx' => 'Sony Playstation',
|
34
35
|
'ps2' => 'Sony Playstation 2',
|
35
36
|
'psp' => 'Sony Playstation Portable',
|
36
|
-
}
|
37
|
+
}.freeze
|
37
38
|
|
38
39
|
def self.platforms
|
39
40
|
PLATFORM
|
40
41
|
end
|
41
42
|
|
42
|
-
def self.
|
43
|
-
|
44
|
-
|
43
|
+
def self.sections
|
44
|
+
('a'..'z').to_a.unshift('0')
|
45
|
+
end
|
45
46
|
|
46
|
-
|
47
|
-
|
48
|
-
page =
|
49
|
-
regions = page.css('input.region').map { |i| i[
|
47
|
+
def self.extract_games(platform)
|
48
|
+
sections.each_with_index do |section, index|
|
49
|
+
page = nokogiri_get("https://coolrom.com.au/roms/#{platform}/#{section}/")
|
50
|
+
regions = page.css('input.region').map { |i| i['name'] }
|
50
51
|
regions.each do |region|
|
51
|
-
|
52
|
-
|
53
|
-
id: game['href'].split('/')[3].to_i,
|
54
|
-
name: game.text,
|
55
|
-
region: region,
|
56
|
-
}
|
57
|
-
}
|
52
|
+
game_links = page.css("div.#{region} a")
|
53
|
+
yield game_links.map { |game_link| game(game_link, region) }, index
|
58
54
|
end
|
59
55
|
end
|
60
|
-
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.game(game_link, region)
|
59
|
+
{
|
60
|
+
id: game_link['href'].split('/')[3].to_i,
|
61
|
+
name: game_link.text,
|
62
|
+
region: region,
|
63
|
+
}
|
61
64
|
end
|
62
65
|
|
63
66
|
def self.install(game)
|
@@ -66,15 +69,16 @@ module Gamerom
|
|
66
69
|
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'
|
67
70
|
|
68
71
|
response = nil
|
69
|
-
agent.progressbar
|
72
|
+
agent.progressbar do
|
70
73
|
response = agent.get("https://coolrom.com.au/downloader.php?id=#{game.id}")
|
71
|
-
}
|
72
|
-
if response.code.to_i == 200
|
73
|
-
filename = response.filename
|
74
|
-
FileUtils.mkdir_p(game.filepath)
|
75
|
-
response.save!("#{game.filepath}/#{filename}")
|
76
|
-
yield [filename]
|
77
74
|
end
|
75
|
+
|
76
|
+
return unless response.code.to_i == 200
|
77
|
+
|
78
|
+
filename = response.filename
|
79
|
+
FileUtils.mkdir_p(game.filepath)
|
80
|
+
response.save!("#{game.filepath}/#{filename}")
|
81
|
+
yield [filename]
|
78
82
|
end
|
79
83
|
end
|
80
84
|
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cgi'
|
4
|
+
require 'mechanize'
|
5
|
+
require 'mechanize/progressbar'
|
6
|
+
require 'mechanizeprogress'
|
7
|
+
|
8
|
+
module Gamerom
|
9
|
+
module RepoAdapters
|
10
|
+
# Romnation - An adapter for the ROMNation repository website
|
11
|
+
class Romnation
|
12
|
+
extend Gamerom::RepoAdapter
|
13
|
+
|
14
|
+
PLATFORM = {
|
15
|
+
'amstrad' => 'Amstrad',
|
16
|
+
'atari2600' => 'Atari 2600',
|
17
|
+
'atari5200' => 'Atari 5200',
|
18
|
+
'atari7800' => 'Atari 7800',
|
19
|
+
'atarijaguar' => 'Atari Jaguar',
|
20
|
+
'atarilynx' => 'Atari Lynx',
|
21
|
+
'colecovision' => 'ColecoVision',
|
22
|
+
'commodore64' => 'Commodore 64',
|
23
|
+
'gamegear' => 'Game Gear',
|
24
|
+
'gb' => 'Game Boy',
|
25
|
+
'gbc' => 'Game Boy Color',
|
26
|
+
'gcdvectrex' => 'Vectrex',
|
27
|
+
'genesis' => 'Genesis',
|
28
|
+
'intellivision' => 'Intellivision',
|
29
|
+
'mame' => 'MAME',
|
30
|
+
'msx1' => 'MSX',
|
31
|
+
'msx2' => 'MSX2',
|
32
|
+
'mtx' => 'MTX',
|
33
|
+
'n64' => 'N64',
|
34
|
+
'neogeocd' => 'Neo Geo CD',
|
35
|
+
'neogeopocket' => 'Neo Geo Pocket',
|
36
|
+
'nes' => 'NES',
|
37
|
+
'oric' => 'Oric',
|
38
|
+
'pce' => 'PC Engine',
|
39
|
+
'radioshackcolorcomputer' => 'TRS-80',
|
40
|
+
'samcoupe' => 'SAM Coupé',
|
41
|
+
'segacd' => 'Sega CD',
|
42
|
+
'segamastersystem' => 'Master System',
|
43
|
+
'snes' => 'SNES',
|
44
|
+
'thompsonmo5' => 'Thomson MO5',
|
45
|
+
'virtualboy' => 'Virtual Boy',
|
46
|
+
'watara' => 'Watara Supervision',
|
47
|
+
'wonderswan' => 'WonderSwan',
|
48
|
+
}.freeze
|
49
|
+
|
50
|
+
def self.platforms
|
51
|
+
PLATFORM
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.sections
|
55
|
+
('a'..'z').to_a.unshift('0')
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.extract_games(platform)
|
59
|
+
sections.each_with_index do |section, index|
|
60
|
+
pages = extract_pages(platform, section)
|
61
|
+
yield extract_games_from_section_pages(platform, section, pages), index
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.extract_pages(platform, section)
|
66
|
+
page = nokogiri_get("https://www.romnation.net/srv/roms/#{platform}/#{section}/sort-title.html")
|
67
|
+
pages = ['1']
|
68
|
+
pages = page.css('.pagination').first.css('a').map(&:text).map(&:strip).reject(&:empty?) unless page.css('.pagination').empty?
|
69
|
+
pages
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.extract_games_from_section_pages(platform, section, pages)
|
73
|
+
pages.reduce([]) do |section_games, p|
|
74
|
+
page = nokogiri_get("https://www.romnation.net/srv/roms/#{platform}/#{section}/page-#{p}_sort-title.html")
|
75
|
+
games_links = page.css('table.listings td.title a')
|
76
|
+
section_games.append(*games_links.map { |game_link| game(game_link) })
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.game(game_link)
|
81
|
+
game_info = GameInfo.new(game_link.text)
|
82
|
+
{
|
83
|
+
id: game_link['href'].split('/')[3].to_i,
|
84
|
+
name: game_link.text,
|
85
|
+
region: game_info.region,
|
86
|
+
tags: game_info.tags,
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.install(game)
|
91
|
+
agent = Mechanize.new
|
92
|
+
agent.pluggable_parser.default = Mechanize::Download
|
93
|
+
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'
|
94
|
+
page = agent.get("https://www.romnation.net/download/rom/#{game.id}")
|
95
|
+
|
96
|
+
response = nil
|
97
|
+
agent.progressbar do
|
98
|
+
response = page.link_with(text: 'Download This Rom').click
|
99
|
+
end
|
100
|
+
|
101
|
+
return unless response.code.to_i == 200
|
102
|
+
|
103
|
+
filename = CGI.unescape(response.filename.split('_host=').first)
|
104
|
+
FileUtils.mkdir_p(game.filepath)
|
105
|
+
response.save!("#{game.filepath}/#{filename}")
|
106
|
+
yield [filename]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|