Soleone-gamefaqs 0.0.2
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.
- data/README.textile +65 -0
- data/lib/gamefaqs.rb +30 -0
- data/lib/gamefaqs/caching.rb +16 -0
- data/lib/gamefaqs/faq.rb +23 -0
- data/lib/gamefaqs/game.rb +31 -0
- data/lib/gamefaqs/list.rb +124 -0
- data/lib/gamefaqs/platform.rb +39 -0
- data/lib/gamefaqs/review.rb +60 -0
- data/lib/gamefaqs/search.rb +61 -0
- data/test/gamefaqs_test.rb +29 -0
- data/test/mocks/reviews_response.html +617 -0
- data/test/mocks/search_response.html +1517 -0
- metadata +72 -0
data/README.textile
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
h1. GameFAQs Library
|
2
|
+
|
3
|
+
Access information about all games (any platform) from GameFAQs.
|
4
|
+
|
5
|
+
You can search for games by title and platform, and then view _Reviews_, _FAQs_, _Cheats_ for it.
|
6
|
+
|
7
|
+
h2. Installation
|
8
|
+
|
9
|
+
@gem install soleone-gamefaqs --source=http://gems.github.com@
|
10
|
+
|
11
|
+
h2. Usage
|
12
|
+
|
13
|
+
<pre>
|
14
|
+
<code>
|
15
|
+
require 'gamefaqs'
|
16
|
+
|
17
|
+
# and for convenience:
|
18
|
+
include GameFaqs
|
19
|
+
</code>
|
20
|
+
</pre>
|
21
|
+
|
22
|
+
h2. Examples
|
23
|
+
|
24
|
+
h3. Find games and platforms
|
25
|
+
|
26
|
+
<pre>
|
27
|
+
<code>
|
28
|
+
# Search for a game containing two words on the Nintendo DS
|
29
|
+
game = GameFaqs::Search.game("Castlevania Ecclesia", "DS")
|
30
|
+
|
31
|
+
# You can also search starting from the platform
|
32
|
+
snes = GameFaqs::Platform.find("snes")
|
33
|
+
game = snes.find("super mario land")
|
34
|
+
</code>
|
35
|
+
</pre>
|
36
|
+
|
37
|
+
h3. Reviews
|
38
|
+
|
39
|
+
<pre>
|
40
|
+
<code>
|
41
|
+
# Get the average score from all reviews
|
42
|
+
game.average_score
|
43
|
+
# Get the average score from only detailed reviews (there are :detailed, :full and :quick)
|
44
|
+
game.average_score(:detailed)
|
45
|
+
|
46
|
+
# Get all reviews for this game
|
47
|
+
reviews = game.reviews
|
48
|
+
# Get only quick reviews for this game
|
49
|
+
reviews = game.reviews(:quick)
|
50
|
+
|
51
|
+
# Get the first review in the list
|
52
|
+
review = reviews.first
|
53
|
+
|
54
|
+
# Score in the format 9/10
|
55
|
+
review.score
|
56
|
+
|
57
|
+
# Get the full text of the review (original html stripped/converted)
|
58
|
+
review.text
|
59
|
+
|
60
|
+
# Other information
|
61
|
+
review.title
|
62
|
+
review.created_at
|
63
|
+
review.author
|
64
|
+
</code>
|
65
|
+
</pre>
|
data/lib/gamefaqs.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
# gems
|
3
|
+
require 'hpricot'
|
4
|
+
|
5
|
+
require 'open-uri'
|
6
|
+
require 'date'
|
7
|
+
|
8
|
+
# load all source files
|
9
|
+
lib = %w[caching platform game search list review]
|
10
|
+
lib.each { |file| require File.join(File.dirname(__FILE__), 'gamefaqs', file) }
|
11
|
+
|
12
|
+
|
13
|
+
module GameFaqs
|
14
|
+
BASE_URL = "http://www.gamefaqs.com"
|
15
|
+
SEARCH_URL = "#{BASE_URL}/search/index.html"
|
16
|
+
|
17
|
+
protected
|
18
|
+
def self.extract_id(url, with_html=true)
|
19
|
+
url.match(/\/([\da-zA-Z]+)#{'\.html' if with_html}$/)
|
20
|
+
$1
|
21
|
+
end
|
22
|
+
|
23
|
+
# 1. convert <br> to \n
|
24
|
+
# 2. convert <b> to * (textile)
|
25
|
+
# 3. convert <i> to _ (textile)
|
26
|
+
# 4. strip all other tags
|
27
|
+
def self.strip_html(string)
|
28
|
+
string.gsub(/<br ?\/?>/, "\n").gsub(/<b>(.+)<\/b>/i, "*\\1*").gsub(/<i>(.+)<\/i>/i, "_\\1_").gsub(/<\/?(\d|\w)+>/i, "")
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module GameFaqs
|
2
|
+
module Caching
|
3
|
+
|
4
|
+
protected
|
5
|
+
# perform the block only when the name isn't filled already
|
6
|
+
def cached_value(name, object=nil, force=false)
|
7
|
+
@@cache ||= {}
|
8
|
+
if force || @@cache[name].nil?
|
9
|
+
@@cache[name] = object
|
10
|
+
return_value = yield object
|
11
|
+
@@cache[name] = return_value if object.nil?
|
12
|
+
end
|
13
|
+
@@cache[name]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/gamefaqs/faq.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module GameFaqs
|
2
|
+
class FAQ
|
3
|
+
attr_reader :game, :id, :type, :title, :created_at, :author, :version, :size
|
4
|
+
|
5
|
+
def initialize(options={})
|
6
|
+
raise ArgumentError.new("Need at least the game and the faq id") unless options[:game] && options[:id]
|
7
|
+
@game, @id, @type, @title, @created_at, @author, @version, @size = options[:game], options[:id], options[:type], options[:title], options[:created_at], options[:author], options[:size]
|
8
|
+
end
|
9
|
+
|
10
|
+
def homepage(refresh=false)
|
11
|
+
cached_value("review-#{@id}-#{@game}", [], refresh) do |homepage|
|
12
|
+
url = "#{@game.homepage.gsub(/game/, 'file').gsub('.html', '')}/#{@id}"
|
13
|
+
doc = Hpricot(open(url))
|
14
|
+
doc.search("a[text()='View/Download Original File'") do |a|
|
15
|
+
puts a.inner_html
|
16
|
+
puts a['href']
|
17
|
+
homepage << a['href']
|
18
|
+
end
|
19
|
+
end.first
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module GameFaqs
|
2
|
+
class Game
|
3
|
+
|
4
|
+
attr_reader :name, :platform, :homepage, :faqs, :codes
|
5
|
+
|
6
|
+
def initialize(name, platform, id)
|
7
|
+
@name = name
|
8
|
+
@platform = platform
|
9
|
+
@id = id
|
10
|
+
@homepage = "#{@platform.homepage}home/#{@id}.html"
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
"#{@name} (#{@platform})"
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
"#{@name} [#{platform}]"
|
19
|
+
end
|
20
|
+
|
21
|
+
def reviews(review_type=nil)
|
22
|
+
List.reviews(self, review_type)
|
23
|
+
end
|
24
|
+
|
25
|
+
def average_score(review_type=nil)
|
26
|
+
sum = reviews(review_type).map{|r| r.score_to_i}.inject{|memo, score| memo + score}
|
27
|
+
sum / reviews(review_type).size.to_f
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module GameFaqs
|
2
|
+
module List
|
3
|
+
extend Caching
|
4
|
+
|
5
|
+
PLATFORMS_PATH = "//ul.systems/li/a"
|
6
|
+
PLATFORMS_ID_PATH = "//form#search/select[@name=platform]/option"
|
7
|
+
GAMES_PATH = "//div.body/table/tr/td/a"
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def platforms(refresh=false)
|
11
|
+
cached_value("platforms", [], refresh) do |platforms|
|
12
|
+
systems_doc = Hpricot(open("#{GameFaqs::BASE_URL}/systems.html"))
|
13
|
+
systems_doc.search(PLATFORMS_PATH).each do |link|
|
14
|
+
name = link.inner_html
|
15
|
+
platforms << Platform.new({:name => name, :homepage => link['href'], :id => platform_ids[name]})
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# find all games (very expensive)
|
21
|
+
def games(platform, refresh=false)
|
22
|
+
cached_value("games", []) do |games|
|
23
|
+
letters = ('a'..'z').to_a << '0'
|
24
|
+
letters.each do |letter|
|
25
|
+
doc = Hpricot(open("#{platform.homepage}list_#{letter}.html"))
|
26
|
+
doc.search(GAMES_PATH).each do |link|
|
27
|
+
name = link.inner_html
|
28
|
+
games << Game.new(name, platform, GameFaqs.extract_id(link['href']))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def reviews(game, type=nil, refresh=false)
|
35
|
+
reviews = cached_value("reviews-#{game.to_s}", [], refresh) do |reviews|
|
36
|
+
url = game.homepage.sub(/\/home\//, "/review/")
|
37
|
+
doc = Hpricot(open(url))
|
38
|
+
doc.search("//div.head/h1") do |h1|
|
39
|
+
header = h1.inner_html
|
40
|
+
|
41
|
+
if header =~ /Reviews/
|
42
|
+
h1.search("../../div.body/table/tr") do |tr|
|
43
|
+
review = {}
|
44
|
+
review[:type] = Review.review_type(header)
|
45
|
+
tr.search("td:eq(0)") do |td|
|
46
|
+
td.search("a") do |a|
|
47
|
+
review[:id] = GameFaqs.extract_id(a['href'])
|
48
|
+
review[:title] = a.inner_html.strip
|
49
|
+
end
|
50
|
+
end
|
51
|
+
tr.search("td:eq(1)") do |td|
|
52
|
+
td.search("a") do |a|
|
53
|
+
review[:author] = a.inner_html.strip
|
54
|
+
end
|
55
|
+
end
|
56
|
+
tr.search("td:eq(2)") do |td|
|
57
|
+
review[:score] = td.inner_html.strip
|
58
|
+
end
|
59
|
+
review[:game] = game
|
60
|
+
reviews << Review.new(review)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
if type
|
66
|
+
types = Review::REVIEW_TYPES
|
67
|
+
raise ArgumentError.new("Type must be one of #{types.join(', ')}") unless types.include?(type.to_sym)
|
68
|
+
reviews.reject { |review| review.type != type.to_sym}
|
69
|
+
else
|
70
|
+
reviews
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def faqs(game, type=nil, refresh=false)
|
75
|
+
faqs = cached_value("faqs-#{game.to_s}", [], refresh) do |faqs|
|
76
|
+
url = game.homepage.sub(/\/home\//, "/game/")
|
77
|
+
doc = Hpricot(open(url))
|
78
|
+
doc.search("//div.head/h1") do |h1|
|
79
|
+
header = h1.inner_html
|
80
|
+
|
81
|
+
h1.search("../../div.body/table/tr") do |tr|
|
82
|
+
faq = {}
|
83
|
+
faq[:type] = header
|
84
|
+
tr.search("td:eq(0)") do |td|
|
85
|
+
td.search("a") do |a|
|
86
|
+
review[:id] = GameFaqs.extract_id(a['href'])
|
87
|
+
review[:title] = a.inner_html.strip
|
88
|
+
end
|
89
|
+
end
|
90
|
+
tr.search("td:eq(1)") do |td|
|
91
|
+
td.search("a") do |a|
|
92
|
+
review[:author] = a.inner_html.strip
|
93
|
+
end
|
94
|
+
end
|
95
|
+
tr.search("td:eq(2)") do |td|
|
96
|
+
review[:score] = td.inner_html.strip
|
97
|
+
end
|
98
|
+
review[:game] = game
|
99
|
+
reviews << Review.new(review)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
if type
|
104
|
+
types = FAQ::FAQ_TYPES
|
105
|
+
raise ArgumentError.new("Type must be one of #{types.join(', ')}") unless types.include?(type.to_sym)
|
106
|
+
faqs.reject { |faq| faq.type != type.to_sym}
|
107
|
+
else
|
108
|
+
faqs
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# find all IDs for every platform (cached, do only once)
|
113
|
+
def platform_ids(refresh=false)
|
114
|
+
cached_value("game_ids", {}, refresh) do |ids|
|
115
|
+
search_doc = Hpricot(open(SEARCH_URL))
|
116
|
+
search_doc.search(PLATFORMS_ID_PATH).each do |option|
|
117
|
+
ids[option['label']] = option['value']
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
end # class < self
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# coupled to List
|
2
|
+
module GameFaqs
|
3
|
+
class Platform
|
4
|
+
attr_reader :name, :homepage, :id
|
5
|
+
|
6
|
+
def initialize(params={})
|
7
|
+
raise ArgumentError("Need at least the name, homepage, and id of the platform!") unless params[:name] && params[:homepage] && params[:id]
|
8
|
+
@name = params[:name]
|
9
|
+
@homepage = "#{GameFaqs::BASE_URL}#{params[:homepage]}"
|
10
|
+
@id = params[:id] if params[:id]
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
@name
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.all
|
18
|
+
List.platforms
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.all_ids
|
22
|
+
List.platform_ids
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.all_games
|
26
|
+
List.games(self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def find(game, refresh=false)
|
30
|
+
Search.game(game, self, refresh)
|
31
|
+
end
|
32
|
+
|
33
|
+
# create case insensitive
|
34
|
+
def self.find(platform_name)
|
35
|
+
Search.platform(platform_name)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module GameFaqs
|
2
|
+
class Review
|
3
|
+
include Caching
|
4
|
+
|
5
|
+
REVIEW_TYPES = [:detailed, :full, :quick]
|
6
|
+
|
7
|
+
attr_reader :game, :id, :score, :author, :title, :type
|
8
|
+
|
9
|
+
def initialize(options={})
|
10
|
+
raise ArgumentError.new("Need at least the game and the review id") unless options[:game] && options[:id]
|
11
|
+
@game, @id, @score, @author, @title, @type = options[:game], options[:id], options[:score], options[:author], options[:title], options[:type]
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
"#{@game}: #{@score} by #{author} (#{@title} [#{@type}])"
|
16
|
+
end
|
17
|
+
|
18
|
+
def score_to_i
|
19
|
+
actual, max = @score.split("/")
|
20
|
+
factor = 10 / max.to_i
|
21
|
+
actual.to_i * factor
|
22
|
+
end
|
23
|
+
|
24
|
+
def text
|
25
|
+
@text ||= parse_review[:text]
|
26
|
+
end
|
27
|
+
|
28
|
+
def created_at
|
29
|
+
@created_at ||= Date.parse(parse_review[:created_at], "%m/%d/%y")
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.all_for(game)
|
33
|
+
List.reviews(game)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.review_type(string)
|
37
|
+
REVIEW_TYPES.each do |type|
|
38
|
+
return type if string =~ /#{type}/i
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def parse_review(refresh=false)
|
44
|
+
cached_value("review-#{@id}-#{@game.platform}", {}, refresh) do |review|
|
45
|
+
url = "#{@game.platform.homepage}review/#{@id}.html"
|
46
|
+
doc = Hpricot(open(url))
|
47
|
+
doc.search("//div.review/div.details") do |div|
|
48
|
+
div.search("p:eq(0)") do |p|
|
49
|
+
review[:text] = GameFaqs.strip_html(p.inner_html)
|
50
|
+
end
|
51
|
+
div.search("p:eq(1)") do |p|
|
52
|
+
date = p.inner_html
|
53
|
+
date.match(/Originally Posted:.*(\d\d\/\d\d\/\d?\d?\d\d)/)
|
54
|
+
review[:created_at] = $1
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module GameFaqs
|
2
|
+
module Search
|
3
|
+
extend Caching
|
4
|
+
class SearchException < Exception; end
|
5
|
+
|
6
|
+
# throws a SearchError when no exact match is found
|
7
|
+
def self.game(game_name, platform, refresh=false)
|
8
|
+
cached_value("search-#{game_name}-#{platform}", nil, refresh) do
|
9
|
+
platform = Platform.find(platform) unless platform.is_a?(Platform)
|
10
|
+
games = []
|
11
|
+
doc = Hpricot(open("#{SEARCH_URL}#{add_params(game_name, platform)}"))
|
12
|
+
doc.search("//div.head/h1") do |h1|
|
13
|
+
if h1.inner_html =~ /Best Matches/
|
14
|
+
h1.search("../../div.body/table") do |table|
|
15
|
+
table.search("tr/td/a") do |a|
|
16
|
+
if a.inner_html =~ /#{game_name.split(' ').join('.*')}/i
|
17
|
+
games << Game.new(a.inner_html.strip, platform, GameFaqs.extract_id(a['href']))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
case games.size
|
25
|
+
when 1
|
26
|
+
games.first
|
27
|
+
when 0
|
28
|
+
raise SearchException.new("Could not find a game containing the string \"#{game_name}\" on platform \"#{platform}\"!")
|
29
|
+
else
|
30
|
+
raise SearchException.new("Found more than one game containing the string \"#{game_name}\": #{games.join(', ')}")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
|
37
|
+
def platform(platform_name)
|
38
|
+
# get by full name (case insensitive)
|
39
|
+
names = List.platforms.select { |p| p.name.downcase == platform_name.downcase }
|
40
|
+
# find other similar if not found exactly one before
|
41
|
+
if names.size != 1
|
42
|
+
names = List.platforms.select{|p| p.name.downcase =~ /#{platform_name.split(' ').join('.*')}/i}
|
43
|
+
end
|
44
|
+
|
45
|
+
case names.size
|
46
|
+
when 1
|
47
|
+
names.first
|
48
|
+
when 0
|
49
|
+
raise SearchException.new("Could not find a platform containing the string \"#{platform_name}\"!")
|
50
|
+
else
|
51
|
+
raise SearchException.new("Found more than one platform containing the string \"#{platform_name}\": #{names.join(', ')}")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
def add_params(keywords, platform)
|
57
|
+
"?game=#{keywords.gsub(/ /, '+')}" << "&platform=#{platform.id}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|