xbox_live 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +150 -0
- data/Rakefile +3 -0
- data/example-usage.rb +20 -0
- data/lib/xbox_live/achievement_info.rb +19 -0
- data/lib/xbox_live/achievements_page.rb +83 -0
- data/lib/xbox_live/game_info.rb +18 -0
- data/lib/xbox_live/games_page.rb +106 -0
- data/lib/xbox_live/profile_page.rb +91 -0
- data/lib/xbox_live/scraper.rb +153 -0
- data/lib/xbox_live/version.rb +3 -0
- data/lib/xbox_live.rb +27 -0
- data/spec/scraper_spec.rb +30 -0
- data/spec/spec_helper.rb +2 -0
- data/xbox_live.gemspec +24 -0
- metadata +94 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
# XboxLive
|
2
|
+
|
3
|
+
XboxLive enables retrieval of player, game, and achievement data from
|
4
|
+
the Xbox Live web site.
|
5
|
+
|
6
|
+
## Status
|
7
|
+
|
8
|
+
This is an early pre-release version! The API is almost certain to
|
9
|
+
change before the 1.0 release.
|
10
|
+
|
11
|
+
Questions and suggestions are welcomed, as are pull requests.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Include the gem in your Gemfile:
|
16
|
+
|
17
|
+
gem "xbox_live"
|
18
|
+
|
19
|
+
Or, if you aren't using Bundler, just run:
|
20
|
+
|
21
|
+
gem install xbox_live
|
22
|
+
|
23
|
+
## Configuration
|
24
|
+
|
25
|
+
An Xbox Live username and password must be provided so that the gem
|
26
|
+
can log into the Xbox Live web site to retrieve data.
|
27
|
+
|
28
|
+
To configure these settings, include the following lines (substituting
|
29
|
+
your information) in your program, or for Rails applications, create
|
30
|
+
a `config/initializers/xbox_live.rb` file and add the lines there:
|
31
|
+
|
32
|
+
# Your Xbox Live login and password
|
33
|
+
XboxLive.options[:username] = 'your@email.address'
|
34
|
+
XboxLive.options[:password] = 'password'
|
35
|
+
|
36
|
+
Two optional configuration options are also available, but are not
|
37
|
+
required to be set:
|
38
|
+
|
39
|
+
# Pages retrieved from Xbox Live are cached for 10 minutes (600
|
40
|
+
# seconds) by default, to prevent unnecessary reloads from the Xbox
|
41
|
+
# Live web site. The maximum cache age can be changed here.
|
42
|
+
XboxLive.options[:refresh_age] = 300 # Cache for only 5 minutes
|
43
|
+
|
44
|
+
# Show debugging output on the console.
|
45
|
+
XboxLive.options[:debug] = true
|
46
|
+
|
47
|
+
## Example
|
48
|
+
|
49
|
+
Below is a short sample stand-alone program to demonstrate basic
|
50
|
+
functionality. This sample program is also included in the git
|
51
|
+
repository.
|
52
|
+
|
53
|
+
require 'xbox_live'
|
54
|
+
|
55
|
+
# Your Xbox Live login and password
|
56
|
+
XboxLive.options[:username] = 'your@email.address'
|
57
|
+
XboxLive.options[:password] = 'password'
|
58
|
+
|
59
|
+
player = 'gamertag'
|
60
|
+
|
61
|
+
profile_page = XboxLive::ProfilePage.new(player)
|
62
|
+
puts "Gamerscore: #{profile_page.gamerscore}"
|
63
|
+
|
64
|
+
games_page = XboxLive::GamesPage.new(player)
|
65
|
+
first_game = games_page.games.first
|
66
|
+
puts "Score in '#{first_game.name}': #{first_game.unlocked_points} out of #{first_game.total_points}"
|
67
|
+
|
68
|
+
achievements_page = XboxLive::AchievementsPage.new(player, first_game.id)
|
69
|
+
first_ach = achievements_page.achievements.first
|
70
|
+
puts "Unlocked achievement '#{first_ach.name}' on #{first_ach.unlocked_on}"
|
71
|
+
|
72
|
+
This will output something along these lines (depending on the gamertag
|
73
|
+
entered for the `player` variable:
|
74
|
+
|
75
|
+
Gamerscore: 9454.
|
76
|
+
Score in 'Battlefield 3': 100 out of 1000.
|
77
|
+
Unlocked achievement '1st Loser' on 10/28/2011.
|
78
|
+
|
79
|
+
## Available Data
|
80
|
+
|
81
|
+
The `XboxLive::ProfilePage` class makes the following data available
|
82
|
+
from a player's Profile page, via a call like `profile_page =
|
83
|
+
XboxLive::ProfilePage.new(gamertag)`:
|
84
|
+
|
85
|
+
* `profile_page.gamertag`
|
86
|
+
* `profile_page.gamerscore`
|
87
|
+
* `profile_page.motto`
|
88
|
+
* `profile_page.avatar`
|
89
|
+
* `profile_page.gamertile_small`
|
90
|
+
* `profile_page.nickname`
|
91
|
+
* `profile_page.bio`
|
92
|
+
* `profile_page.presence`
|
93
|
+
|
94
|
+
The `XboxLive::GamesPage` class makes the following data available from
|
95
|
+
a player's Game Comparison page, via a call like `games_page =
|
96
|
+
XboxLive::GamesPage.new(gamertag)`:
|
97
|
+
|
98
|
+
* `games_page.gamertag`
|
99
|
+
* `games_page.gamertile_large`
|
100
|
+
* `games_page.gamerscore`
|
101
|
+
* `games_page.progress`
|
102
|
+
* `games_page.games` _(see below)_
|
103
|
+
|
104
|
+
`games_page.games` is an Array of XboxLive::GameInfo instances, which track information about a
|
105
|
+
player's progress in a game. Each GameInfo instance makes the following data available:
|
106
|
+
|
107
|
+
* `game_info.id` - unique Microsoft identifier
|
108
|
+
* `game_info.name`
|
109
|
+
* `game_info.tile`
|
110
|
+
* `game_info.total_points`
|
111
|
+
* `game_info.total_achievements`
|
112
|
+
* `game_info.gamertag`
|
113
|
+
* `game_info.unlocked_points`
|
114
|
+
* `game_info.unlocked_achievements`
|
115
|
+
* `game_info.last_played`
|
116
|
+
|
117
|
+
The `XboxLive::AchievementsPage` class makes the following data
|
118
|
+
available from a player's Game Achievement Comparison page, via a call
|
119
|
+
like `ach_page = XboxLive::AchievementsPage.new(gamertag, game_id)`:
|
120
|
+
|
121
|
+
* `ach_page.gamertag`
|
122
|
+
* `ach_page.game_id`
|
123
|
+
* `ach_page.achievements` _(see below)_
|
124
|
+
|
125
|
+
`ach_page.achievements` is an Array of XboxLive::AchievementInfo
|
126
|
+
instances, which track information about a player's achievements in a
|
127
|
+
game. Each AchievementInfo instance makes the following data available:
|
128
|
+
|
129
|
+
* `ach_info.id` - Microsoft identifier, unique only within this game
|
130
|
+
* `ach_info.gamertag`
|
131
|
+
* `ach_info.game_id`
|
132
|
+
* `ach_info.name`
|
133
|
+
* `ach_info.description`
|
134
|
+
* `ach_info.tile`
|
135
|
+
* `ach_info.points`
|
136
|
+
* `ach_info.unlocked_at` - nil if the player has not yet unlocked it
|
137
|
+
|
138
|
+
## Caveats
|
139
|
+
|
140
|
+
The contents, layout, and authentication scheme for the Xbox Live web
|
141
|
+
site may change at any time, and historically has changed several times
|
142
|
+
per year. These changes will almost certainly break the functionality
|
143
|
+
of this gem, requiring a new version of the gem to be coded and
|
144
|
+
released.
|
145
|
+
|
146
|
+
## To Do for Version 1.0
|
147
|
+
|
148
|
+
* Write tests
|
149
|
+
* Refactor
|
150
|
+
* Improve API
|
data/Rakefile
ADDED
data/example-usage.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'xbox_live'
|
2
|
+
|
3
|
+
# Your Xbox Live login and password
|
4
|
+
XboxLive.options[:username] = 'your@email.address'
|
5
|
+
XboxLive.options[:password] = 'password'
|
6
|
+
XboxLive.options[:debug] = false
|
7
|
+
|
8
|
+
player = 'gamertag'
|
9
|
+
|
10
|
+
profile_page = XboxLive::ProfilePage.new(player)
|
11
|
+
puts "Gamerscore: #{profile_page.gamerscore}"
|
12
|
+
|
13
|
+
games_page = XboxLive::GamesPage.new(player)
|
14
|
+
first_game = games_page.games.first
|
15
|
+
puts "Score in '#{first_game.name}': #{first_game.unlocked_points} out of #{first_game.total_points}"
|
16
|
+
|
17
|
+
achievements_page = XboxLive::AchievementsPage.new(player, first_game.id)
|
18
|
+
first_ach = achievements_page.achievements.first
|
19
|
+
puts "Unlocked achievement '#{first_ach.name}' on #{first_ach.unlocked_on}"
|
20
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module XboxLive
|
2
|
+
|
3
|
+
# Each AchievementInfo tracks information about a player's progress in a
|
4
|
+
# specific achievement.
|
5
|
+
class AchievementInfo
|
6
|
+
|
7
|
+
attr_accessor :id, :game_id, :gamertag, :name, :description, :tile, :points, :unlocked_at
|
8
|
+
|
9
|
+
# Create a new AchievementInfo for the provided player and game.
|
10
|
+
def initialize(gamertag, game_id, achievement_id)
|
11
|
+
@gamertag = gamertag
|
12
|
+
@game_id = game_id
|
13
|
+
@id = achievement_id
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module XboxLive
|
2
|
+
|
3
|
+
# Each AchievementsPage tracks and makes available the data contianed
|
4
|
+
# in an Xbox Live "Compare Game" page. This can be used to determine
|
5
|
+
# which achievements a player has unlocked in a game.
|
6
|
+
#
|
7
|
+
# Example: http://live.xbox.com/en-US/Activity/Details?titleId=1161890128&compareTo=someone
|
8
|
+
class AchievementsPage
|
9
|
+
|
10
|
+
attr_accessor :gamertag, :game_id, :page, :url, :updated_at, :achievements, :data
|
11
|
+
|
12
|
+
# Create a new AchievementsPage for the provided gamertag. Retrieve
|
13
|
+
# the html compare achievements page from the Xbox Live web site for
|
14
|
+
# analysis. To prevent multiple instances for the same gamertag,
|
15
|
+
# this method is marked as private. The AchievementsPage.find()
|
16
|
+
# method should be used to find an existing instance or create a new
|
17
|
+
# one if needed.
|
18
|
+
def initialize(gamertag, game_id)
|
19
|
+
@gamertag = gamertag
|
20
|
+
@game_id = game_id
|
21
|
+
refresh
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
# Force a reload of the AchievementsPage data from the Xbox Live web site.
|
26
|
+
def refresh
|
27
|
+
url = XboxLive.options[:url_prefix] + '/en-US/Activity/Details?' +
|
28
|
+
Mechanize::Util.build_query_string(titleId: @game_id, compareTo: @gamertag)
|
29
|
+
@page = XboxLive::Scraper::get_page url
|
30
|
+
return false if page.nil?
|
31
|
+
|
32
|
+
@url = url
|
33
|
+
@updated_at = Time.now
|
34
|
+
@data = retrieve_achievement_data
|
35
|
+
@gamertag = find_gamertag
|
36
|
+
@achievements = find_achievements
|
37
|
+
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# POST to retrieve the JSON data about achievements for this game
|
45
|
+
def retrieve_achievement_data
|
46
|
+
data = @page.body.match(/loadCompareView\((.+)\)\;/)[1]
|
47
|
+
JSON.parse(data)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Find the gamertag, in case the caps/lowercase are different than
|
51
|
+
# what was provided.
|
52
|
+
def find_gamertag
|
53
|
+
player = @data['Players'].find { |p| p['Gamertag'].casecmp(@gamertag) == 0 }
|
54
|
+
player ? player['Gamertag'] : nil
|
55
|
+
end
|
56
|
+
|
57
|
+
# Find and return an array of hashes containing information about each
|
58
|
+
# achievement the player has unlocked.
|
59
|
+
def find_achievements
|
60
|
+
achievements = @data['Achievements'].collect do |ach|
|
61
|
+
ai = AchievementInfo.new(gamertag, @game_id, ach['Id'])
|
62
|
+
ai.name = ach['Name']
|
63
|
+
ai.description = ach['Description']
|
64
|
+
ai.tile = ach['TileUrl']
|
65
|
+
if unlocked?(ach)
|
66
|
+
ai.points = ach['Score']
|
67
|
+
# TODO: Refactor this mess
|
68
|
+
time_field = ach['EarnDates'][@gamertag]['EarnedOn'].match(/Date\((\d+)/)
|
69
|
+
ai.unlocked_at = Time.at(time_field[1].to_i / 1000) if time_field
|
70
|
+
end
|
71
|
+
ai
|
72
|
+
end
|
73
|
+
achievements
|
74
|
+
end
|
75
|
+
|
76
|
+
# Has the player unlocked this achievement?
|
77
|
+
def unlocked?(ach)
|
78
|
+
!!ach['EarnDates'][@gamertag]
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module XboxLive
|
2
|
+
|
3
|
+
# Each GameInfo tracks information about a player's progress in a
|
4
|
+
# specific game.
|
5
|
+
class GameInfo
|
6
|
+
|
7
|
+
attr_accessor :id, :name, :tile, :gamertag, :total_points, :unlocked_points,
|
8
|
+
:total_achievements, :unlocked_achievements, :last_played
|
9
|
+
|
10
|
+
# Create a new GameInfo for the provided player and game.
|
11
|
+
def initialize(gamertag, game_id)
|
12
|
+
@gamertag = gamertag
|
13
|
+
@id = game_id
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module XboxLive
|
2
|
+
|
3
|
+
# Each GamesPage tracks the data contianed in an Xbox Live "Compare
|
4
|
+
# Games" page. This can be used to determine which games a player has
|
5
|
+
# played, and their score and number of achievements acquired in each
|
6
|
+
# game.
|
7
|
+
#
|
8
|
+
# Example: http://live.xbox.com/en-US/Activity?compareTo=someone
|
9
|
+
class GamesPage
|
10
|
+
|
11
|
+
attr_accessor :gamertag, :page, :url, :updated_at, :gamertile_large,
|
12
|
+
:gamerscore, :progress, :games, :data
|
13
|
+
|
14
|
+
|
15
|
+
# Create a new GamesPage for the provided gamertag. Retrieve the
|
16
|
+
# html game compare page from the Xbox Live web site for analysis.
|
17
|
+
def initialize(gamertag)
|
18
|
+
@gamertag = gamertag
|
19
|
+
refresh
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
# Force a reload of the GamesPage data from the Xbox Live web site.
|
24
|
+
def refresh
|
25
|
+
url = XboxLive.options[:url_prefix] + '/en-US/Activity?' +
|
26
|
+
Mechanize::Util.build_query_string(compareTo: @gamertag)
|
27
|
+
@page = XboxLive::Scraper::get_page(url)
|
28
|
+
return false if page.nil?
|
29
|
+
|
30
|
+
@url = url
|
31
|
+
@updated_at = Time.now
|
32
|
+
@data = retrieve_game_data
|
33
|
+
@gamertag = find_gamertag
|
34
|
+
@gamertile_large = find_gamertile_large
|
35
|
+
@gamerscore = find_gamerscore
|
36
|
+
@progress = find_progress
|
37
|
+
@games = find_games
|
38
|
+
|
39
|
+
return true
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# POST to retrieve the JSON data about games that have been played
|
46
|
+
def retrieve_game_data
|
47
|
+
if token = find_request_verification_token
|
48
|
+
url = XboxLive.options[:url_prefix] + '/en-US/Activity/Summary?' +
|
49
|
+
Mechanize::Util.build_query_string(compareTo: @gamertag)
|
50
|
+
page = XboxLive::Scraper::post_page(url, '__RequestVerificationToken' => token)
|
51
|
+
end
|
52
|
+
JSON.parse(page.body)['Data']
|
53
|
+
end
|
54
|
+
|
55
|
+
# Find the RequestVerificationToken
|
56
|
+
def find_request_verification_token
|
57
|
+
token_block = @page.at('input[name=__RequestVerificationToken]')
|
58
|
+
token_block ? token_block.get_attribute('value') : nil
|
59
|
+
end
|
60
|
+
|
61
|
+
# Find the gamertag, in case the caps/lowercase are different than
|
62
|
+
# what was provided.
|
63
|
+
def find_gamertag
|
64
|
+
player = @data['Players'].find { |p| p['Gamertag'].casecmp(@gamertag) == 0 }
|
65
|
+
player ? player['Gamertag'] : nil
|
66
|
+
end
|
67
|
+
|
68
|
+
# Find and return the player's large gamertile url from the Games data
|
69
|
+
def find_gamertile_large
|
70
|
+
player = @data['Players'].find { |p| p['Gamertag'] == @gamertag }
|
71
|
+
player ? player['Gamerpic'] : nil
|
72
|
+
end
|
73
|
+
|
74
|
+
# Find and return the player's gamerscore from the Games page
|
75
|
+
def find_gamerscore
|
76
|
+
player = @data['Players'].find { |p| p['Gamertag'] == @gamertag }
|
77
|
+
player ? player['Gamerscore'] : nil
|
78
|
+
end
|
79
|
+
|
80
|
+
# Find and return the player's game progress statistic from the Games page
|
81
|
+
def find_progress
|
82
|
+
player = @data['Players'].find { |p| p['Gamertag'] == @gamertag }
|
83
|
+
player ? player['PercentComplete'] : nil
|
84
|
+
end
|
85
|
+
|
86
|
+
# Find and return an array of hashes containing information about each
|
87
|
+
# game the player has played.
|
88
|
+
def find_games
|
89
|
+
games = @data['Games'].collect do |game|
|
90
|
+
gi = GameInfo.new(gamertag, game['Id'])
|
91
|
+
gi.name = game['Name']
|
92
|
+
gi.tile = game['BoxArt']
|
93
|
+
gi.total_points = game['PossibleScore']
|
94
|
+
gi.total_achievements = game['PossibleAchievements']
|
95
|
+
gi.unlocked_points = game['Progress'][@gamertag]['Score']
|
96
|
+
gi.unlocked_achievements = game['Progress'][@gamertag]['Achievements']
|
97
|
+
time_field = game['Progress'][@gamertag]['LastPlayed'].match(/Date\((\d+)/)
|
98
|
+
gi.last_played = Time.at(time_field[1].to_i / 1000) if time_field
|
99
|
+
gi
|
100
|
+
end
|
101
|
+
return games
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module XboxLive
|
2
|
+
|
3
|
+
# Each ProfilePage tracks and makes available the data contained in an Xbox
|
4
|
+
# Live profile web page. This can be used to determine general information
|
5
|
+
# about a payer, such as their total score, avatar picture, or bio.
|
6
|
+
#
|
7
|
+
# Example: http://live.xbox.com/en-US/Profile?Gamertag=someone
|
8
|
+
class ProfilePage
|
9
|
+
|
10
|
+
attr_accessor :gamertag, :page, :url, :updated_at, :gamerscore, :motto,
|
11
|
+
:avatar, :gamertile_small, :nickname, :bio, :presence
|
12
|
+
|
13
|
+
|
14
|
+
# Create a new ProfilePage for the provided gamertag. Retrieve the
|
15
|
+
# html profile page from the Xbox Live web site for analysis.
|
16
|
+
def initialize(gamertag)
|
17
|
+
@gamertag = gamertag
|
18
|
+
refresh
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
# Force a reload of the ProfilePage data from the Xbox Live web site.
|
23
|
+
#
|
24
|
+
# TODO: Parse the Location: and reputation fields as well.
|
25
|
+
def refresh
|
26
|
+
url = XboxLive.options[:url_prefix] + '/en-US/Profile?' +
|
27
|
+
Mechanize::Util.build_query_string(gamertag: @gamertag)
|
28
|
+
@page = XboxLive::Scraper::get_page url
|
29
|
+
return false if @page.nil?
|
30
|
+
|
31
|
+
@url = url
|
32
|
+
@updated_at = Time.now
|
33
|
+
@gamerscore = find_gamerscore
|
34
|
+
@motto = find_motto
|
35
|
+
@avatar = find_avatar
|
36
|
+
@nickname = find_nickname
|
37
|
+
@bio = find_bio
|
38
|
+
@presence = find_presence
|
39
|
+
@gamertile_small = find_gamertile_small
|
40
|
+
|
41
|
+
return true
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Find and return the player's gamerscore from the ProfilePage
|
47
|
+
def find_gamerscore
|
48
|
+
score_block = @page.at('div.gamerscore')
|
49
|
+
score_block ? score_block.inner_html.to_i : nil
|
50
|
+
end
|
51
|
+
|
52
|
+
# Find and return the player's motto from the ProfilePage
|
53
|
+
def find_motto
|
54
|
+
motto_block = @page.at('div.motto')
|
55
|
+
# TODO: Need to strip out the empty bubble-arrow div
|
56
|
+
motto_block ? motto_block.inner_html.strip : nil
|
57
|
+
end
|
58
|
+
|
59
|
+
# Find and return the player's avatar url from the ProfilePage
|
60
|
+
def find_avatar
|
61
|
+
# FIXME: Not currently working. Javascript?
|
62
|
+
avatar_block = @page.at('img.bodyshot')
|
63
|
+
avatar_block ? avatar_block.get_attribute('src') : nil
|
64
|
+
end
|
65
|
+
|
66
|
+
# Find and return the player's small gamertile url from the ProfilePage
|
67
|
+
def find_gamertile_small
|
68
|
+
tile_block = @page.at('img.gamerpic')
|
69
|
+
tile_block ? tile_block.get_attribute('src') : nil
|
70
|
+
end
|
71
|
+
|
72
|
+
# Find and return the player's nickname from the ProfilePage
|
73
|
+
def find_nickname
|
74
|
+
nickname_block = @page.at('div.name div.value')
|
75
|
+
nickname_block ? nickname_block.inner_html.strip : nil
|
76
|
+
end
|
77
|
+
|
78
|
+
# Find and return the player's bio from the ProfilePage
|
79
|
+
def find_bio
|
80
|
+
bio_block = @page.at('div.bio div.value')
|
81
|
+
bio_block ? bio_block.inner_html.strip : nil
|
82
|
+
end
|
83
|
+
|
84
|
+
# Find and return the player's most recent presence info from the ProfilePage
|
85
|
+
def find_presence
|
86
|
+
presence_block = @page.at('div.presence')
|
87
|
+
presence_block ? presence_block.inner_html.strip : nil
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
module XboxLive
|
2
|
+
|
3
|
+
# Scraper is a collection of methods to log into the Xbox Live web site
|
4
|
+
# and retrieve web pages.
|
5
|
+
#
|
6
|
+
# The only public function is XboxLive::Scraper.get_page(url)
|
7
|
+
module Scraper
|
8
|
+
|
9
|
+
# Since loading pages from the Xbox Live web site is expensive
|
10
|
+
# (slow), pages should be cached for a short amount of time in case
|
11
|
+
# they are re-requested again.
|
12
|
+
@cache = Hash.new
|
13
|
+
|
14
|
+
# Load a page from Xbox Live and return a Mechanize/Nokogiri page
|
15
|
+
# TODO: cache pages for some time to prevent duplicative HTTP activity
|
16
|
+
def self.get_page(url)
|
17
|
+
log "Loading page #{url}."
|
18
|
+
|
19
|
+
# Check to see if there is a recent version of the page in cache
|
20
|
+
if @cache[url]
|
21
|
+
log " Found page in cache."
|
22
|
+
return @cache[url][:page] if Time.now - @cache[url][:updated_at] < XboxLive.options[:refresh_age]
|
23
|
+
log " but the cached page is stale."
|
24
|
+
end
|
25
|
+
|
26
|
+
# Load the specified page via Mechanize
|
27
|
+
log " Getting page from Xbox Live."
|
28
|
+
page = safe_get(url)
|
29
|
+
|
30
|
+
# Most pages require authentication. If the Mechanize agent has
|
31
|
+
# not logged in yet, or if the session has expired, it will be
|
32
|
+
# redirected to the Xbox Live login page.
|
33
|
+
if login_page?(page)
|
34
|
+
# Log the agent in via the returned login page.
|
35
|
+
log " Page load failed - not signed in."
|
36
|
+
page = login(page)
|
37
|
+
|
38
|
+
# The login SHOULD have returned the original page requested,
|
39
|
+
# but the URL will be the POST URL, so there is no way to be
|
40
|
+
# certain. Therefore, it is safest to just load the page again
|
41
|
+
# now that the Mechanize agent has logged in.
|
42
|
+
log " Retrying page #{url}"
|
43
|
+
page = safe_get(url)
|
44
|
+
end
|
45
|
+
|
46
|
+
if page.nil? or page.title.match /Error/
|
47
|
+
log " ERROR: failed to load page. Trying again."
|
48
|
+
page = safe_get(url)
|
49
|
+
if page.nil? or page.title.match /Error/
|
50
|
+
log " ERROR: failed on second try. Aborting."
|
51
|
+
return nil
|
52
|
+
else
|
53
|
+
log " SUCCESS: page loaded on retry."
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
if page.uri.to_s != url
|
58
|
+
log " ERROR: loaded page URL does not match expected URL. Loaded: #{page.uri.to_s}"
|
59
|
+
return nil
|
60
|
+
end
|
61
|
+
|
62
|
+
log " Loaded page '#{page.title.strip}'. Storing in cache."
|
63
|
+
@cache[url] = { page: page, updated_at: Time.now }
|
64
|
+
page
|
65
|
+
end
|
66
|
+
|
67
|
+
# POST a page to Xbox Live and return the result.
|
68
|
+
def self.post_page(url, params)
|
69
|
+
log "POSTing page #{url} with params #{params}."
|
70
|
+
page = agent.post(url, params)
|
71
|
+
page
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
# private
|
76
|
+
|
77
|
+
# Get a page, but catch any errors so processing can continue
|
78
|
+
def self.safe_get(page)
|
79
|
+
begin
|
80
|
+
return agent.get(page)
|
81
|
+
# rescue Errno::ETIMEDOUT, Timeout::Error, Mechanize::ResponseCodeError
|
82
|
+
rescue
|
83
|
+
return nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Log in to Xbox Live using the supplied login page.
|
88
|
+
def self.login(page)
|
89
|
+
return nil if !login_page?(page)
|
90
|
+
|
91
|
+
# Find the URL where the login form should be POSTed to.
|
92
|
+
url = page.body.match(/srf_uPost='([^']+)/)[1]
|
93
|
+
if url.empty?
|
94
|
+
log " ERROR: Trying to log in but 'Sign In' page doesn't contain needed info."
|
95
|
+
return nil
|
96
|
+
end
|
97
|
+
|
98
|
+
# PPFT appears to be some kind of session identifier which is
|
99
|
+
# required for the login process.
|
100
|
+
ppft_html = page.body.match(/srf_sFT='([^']+)/)[1]
|
101
|
+
ppft = ppft_html.match(/value="([^"]+)/)[1]
|
102
|
+
|
103
|
+
# The rest of the parameters are either user-provided (i.e.
|
104
|
+
# username and password) or are constants.
|
105
|
+
params = {
|
106
|
+
'login' => XboxLive.options[:username],
|
107
|
+
'passwd' => XboxLive.options[:password],
|
108
|
+
'type' => '11',
|
109
|
+
'LoginOptions' => '3',
|
110
|
+
'NewUser' => '1',
|
111
|
+
'PPSX' => 'Passpor',
|
112
|
+
'PPFT' => ppft,
|
113
|
+
'idshbo' => '1'
|
114
|
+
}
|
115
|
+
|
116
|
+
# POST the login form and hope for the best.
|
117
|
+
log " Submitting login form via POST"
|
118
|
+
page = agent.post(url, params)
|
119
|
+
|
120
|
+
# The login will fail and return a page saying that Javascript must be
|
121
|
+
# enabled. However, there is a hidden form in the page that can be
|
122
|
+
# submitted to enable non-javascript support.
|
123
|
+
form = page.form('fmHF')
|
124
|
+
if form.nil?
|
125
|
+
log " ERROR: The non-JS login page doesn't contain form fmHF."
|
126
|
+
return nil
|
127
|
+
end
|
128
|
+
|
129
|
+
# Submitting the form on the Javascript error page completes the
|
130
|
+
# login process, and SHOULD return the originally requested page.
|
131
|
+
log " Submitting final non-JS login form"
|
132
|
+
agent.submit(form)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Check to see if the provided page the Xbox Live login page.
|
136
|
+
def self.login_page?(page)
|
137
|
+
page and page.title == "Welcome to Windows Live"
|
138
|
+
end
|
139
|
+
|
140
|
+
# Create and memoize the Mechanize agent
|
141
|
+
def self.agent
|
142
|
+
log " Initializing mechanize agent @ #{Time.now.to_s}" if !defined? @@agent
|
143
|
+
@@agent ||= Mechanize.new { |a| a.user_agent_alias = 'Mac Safari' }
|
144
|
+
end
|
145
|
+
|
146
|
+
# Write out a log entry
|
147
|
+
def self.log(message)
|
148
|
+
puts message if XboxLive.options[:debug]
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
data/lib/xbox_live.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'mechanize'
|
2
|
+
require 'json'
|
3
|
+
require 'xbox_live/version'
|
4
|
+
require 'xbox_live/scraper'
|
5
|
+
require 'xbox_live/game_info'
|
6
|
+
require 'xbox_live/achievement_info'
|
7
|
+
require 'xbox_live/profile_page'
|
8
|
+
require 'xbox_live/games_page'
|
9
|
+
require 'xbox_live/achievements_page'
|
10
|
+
|
11
|
+
module XboxLive
|
12
|
+
|
13
|
+
# Provides configurability.
|
14
|
+
def self.options
|
15
|
+
@options ||= {
|
16
|
+
:username => nil,
|
17
|
+
:password => nil,
|
18
|
+
:refresh_age => 600, # data will be re-fetched if older than X seconds
|
19
|
+
:url_prefix => 'http://live.xbox.com'
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
XboxLive.options[:username] = 'xboxlive@mfischer.com'
|
24
|
+
XboxLive.options[:password] = 's9dALtmG'
|
25
|
+
XboxLive.options[:debug] = true
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe XboxLive::Scraper do
|
4
|
+
|
5
|
+
describe "#get_page" do
|
6
|
+
|
7
|
+
context "with a public (non-authenticated) page" do
|
8
|
+
before { @page = XboxLive::Scraper.get_page('http://live.xbox.com/en-US/MyXbox/Profile?gamertag=major%20nelson') }
|
9
|
+
|
10
|
+
it 'should return a Mechanize::Page instance' do
|
11
|
+
@page.class.should == Mechanize::Page
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should return the page with the expected title' do
|
15
|
+
@page.title.strip.should == 'Major Nelson - Xbox.com'
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
context "with a private (authenticated) page" do
|
21
|
+
before { @page = XboxLive::Scraper.get_page('http://live.xbox.com/en-US/MyXbox/Profile?gamertag=major%20nelson') }
|
22
|
+
|
23
|
+
it 'should return the page with the expected title' do
|
24
|
+
@page.title.strip.should == 'Major Nelson - Xbox.com'
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
data/spec/spec_helper.rb
ADDED
data/xbox_live.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "xbox_live/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "xbox_live"
|
7
|
+
s.version = XboxLive::VERSION
|
8
|
+
s.authors = ["Mike Fischer"]
|
9
|
+
s.email = ["mikefischer99@gmail.com"]
|
10
|
+
s.homepage = "https://github.com/greendog99/xbox_live"
|
11
|
+
s.summary = %q{Xbox Live data retrieval}
|
12
|
+
s.description = %q{Log into Xbox Live and retrieve information about a player}
|
13
|
+
s.platform = Gem::Platform::RUBY
|
14
|
+
s.rubyforge_project = "xbox_live"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {spec}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_runtime_dependency "mechanize", "~> 1.0"
|
22
|
+
s.add_runtime_dependency "json"
|
23
|
+
s.add_development_dependency "rspec", "~> 2.6"
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: xbox_live
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.3
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Mike Fischer
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-11-13 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: mechanize
|
16
|
+
requirement: &70358515052260 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70358515052260
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: json
|
27
|
+
requirement: &70358515051320 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70358515051320
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rspec
|
38
|
+
requirement: &70358515050680 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '2.6'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70358515050680
|
47
|
+
description: Log into Xbox Live and retrieve information about a player
|
48
|
+
email:
|
49
|
+
- mikefischer99@gmail.com
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- .gitignore
|
55
|
+
- Gemfile
|
56
|
+
- README.md
|
57
|
+
- Rakefile
|
58
|
+
- example-usage.rb
|
59
|
+
- lib/xbox_live.rb
|
60
|
+
- lib/xbox_live/achievement_info.rb
|
61
|
+
- lib/xbox_live/achievements_page.rb
|
62
|
+
- lib/xbox_live/game_info.rb
|
63
|
+
- lib/xbox_live/games_page.rb
|
64
|
+
- lib/xbox_live/profile_page.rb
|
65
|
+
- lib/xbox_live/scraper.rb
|
66
|
+
- lib/xbox_live/version.rb
|
67
|
+
- spec/scraper_spec.rb
|
68
|
+
- spec/spec_helper.rb
|
69
|
+
- xbox_live.gemspec
|
70
|
+
homepage: https://github.com/greendog99/xbox_live
|
71
|
+
licenses: []
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ! '>='
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
requirements: []
|
89
|
+
rubyforge_project: xbox_live
|
90
|
+
rubygems_version: 1.8.10
|
91
|
+
signing_key:
|
92
|
+
specification_version: 3
|
93
|
+
summary: Xbox Live data retrieval
|
94
|
+
test_files: []
|