bnet_scraper 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/CHANGELOG.md +12 -0
  2. data/README.md +1 -1
  3. data/bnet_scraper.gemspec +3 -3
  4. data/lib/bnet_scraper/starcraft2.rb +42 -39
  5. data/lib/bnet_scraper/starcraft2/achievement.rb +24 -0
  6. data/lib/bnet_scraper/starcraft2/achievement_scraper.rb +50 -26
  7. data/lib/bnet_scraper/starcraft2/base_scraper.rb +34 -18
  8. data/lib/bnet_scraper/starcraft2/match_history_scraper.rb +22 -13
  9. data/lib/bnet_scraper/starcraft2/profile.rb +19 -1
  10. data/lib/bnet_scraper/starcraft2/profile_scraper.rb +102 -38
  11. data/lib/bnet_scraper/starcraft2/status_scraper.rb +4 -3
  12. data/spec/spec_helper.rb +1 -1
  13. data/spec/starcraft2/achievement_scraper_spec.rb +33 -23
  14. data/spec/starcraft2/match_history_scraper_spec.rb +2 -0
  15. data/spec/starcraft2/profile_scraper_spec.rb +18 -11
  16. data/spec/starcraft2/profile_spec.rb +13 -0
  17. data/spec/starcraft2/status_scraper_spec.rb +2 -2
  18. data/{fixtures → spec/support/fixtures}/vcr_cassettes/demon_achievements.yml +0 -0
  19. data/{fixtures → spec/support/fixtures}/vcr_cassettes/demon_leagues.yml +0 -0
  20. data/{fixtures → spec/support/fixtures}/vcr_cassettes/demon_match_history.yml +0 -0
  21. data/{fixtures → spec/support/fixtures}/vcr_cassettes/demon_matches.yml +0 -0
  22. data/spec/support/fixtures/vcr_cassettes/demon_profile.yml +1768 -0
  23. data/{fixtures → spec/support/fixtures}/vcr_cassettes/demon_profile_leagues.yml +0 -0
  24. data/{fixtures → spec/support/fixtures}/vcr_cassettes/full_demon_scrape.yml +0 -0
  25. data/{fixtures → spec/support/fixtures}/vcr_cassettes/invalid_achievement.yml +0 -0
  26. data/{fixtures → spec/support/fixtures}/vcr_cassettes/invalid_leagues.yml +0 -0
  27. data/{fixtures → spec/support/fixtures}/vcr_cassettes/invalid_matches.yml +0 -0
  28. data/{fixtures → spec/support/fixtures}/vcr_cassettes/invalid_profile.yml +0 -0
  29. data/{fixtures → spec/support/fixtures}/vcr_cassettes/new_league.yml +0 -0
  30. data/{fixtures → spec/support/fixtures}/vcr_cassettes/profile_invalid.yml +0 -0
  31. data/{fixtures → spec/support/fixtures}/vcr_cassettes/profile_not_laddered.yml +0 -0
  32. data/{fixtures → spec/support/fixtures}/vcr_cassettes/realm_status.yml +0 -0
  33. metadata +86 -35
  34. data/fixtures/vcr_cassettes/demon_profile.yml +0 -1428
@@ -1,5 +1,17 @@
1
1
  # Changelog!
2
2
 
3
+ ## 0.5.0 (Mar 29 2013)
4
+
5
+ * Adds Heart of the Swarm Portrait Names
6
+ * Updates Achievement Progress Categories for Heart of the Swarm
7
+ * Adds Achievement to replace hash output
8
+ * Replace FakeWeb specs with VCR
9
+ * Typecast numerics to Fixnum instead of strings
10
+ * Typecast dates to Date instead of Strings (`Achievement#earned=`)
11
+ * Fix wins/losses in MatchHistoryScraper
12
+ * Adds Campaign Completion indicators (`Profile#campaign_completion`)
13
+ * Extensive internal refactoring and decoupling
14
+
3
15
  ## 0.4.0 (Feb 22 2013)
4
16
 
5
17
  * Revamped ProfileScraper API
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # BnetScraper [![Build Status](https://secure.travis-ci.org/agoragames/bnet_scraper.png)](http://travis-ci.org/agoragames/bnet_scraper)
1
+ # BnetScraper [![Build Status](https://secure.travis-ci.org/agoragames/bnet_scraper.png)](http://travis-ci.org/agoragames/bnet_scraper) [![Code Climate](https://codeclimate.com/github/agoragames/bnet_scraper.png)](https://codeclimate.com/github/agoragames/bnet_scraper)
2
2
 
3
3
  BnetScraper is a Nokogiri-based scraper of Battle.net profile information. Currently this only includes Starcraft2.
4
4
 
@@ -3,9 +3,9 @@ $:.push File.expand_path("../lib", __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "bnet_scraper"
6
- s.version = "0.4.0"
6
+ s.version = "0.5.0"
7
7
  s.authors = ["Andrew Nordman"]
8
- s.email = ["anordman@majorleaguegaming.com"]
8
+ s.email = ["cadwallion@gmail.com"]
9
9
  s.homepage = "https://github.com/agoragames/bnet_scraper/"
10
10
  s.summary = %q{Battle.net Profile Scraper}
11
11
  s.description = %q{BnetScraper is a Nokogiri-based scraper of Battle.net profile information. Currently this only includes Starcraft2.}
@@ -18,8 +18,8 @@ Gem::Specification.new do |s|
18
18
  s.add_runtime_dependency 'nokogiri'
19
19
  s.add_runtime_dependency 'faraday'
20
20
  s.add_development_dependency 'rake'
21
- s.add_development_dependency 'rspec'
22
21
  s.add_development_dependency 'fakeweb'
22
+ s.add_development_dependency 'rspec'
23
23
  s.add_development_dependency 'vcr'
24
24
  s.add_development_dependency 'pry'
25
25
  end
@@ -38,47 +38,50 @@ module BnetScraper
38
38
  # I decided th pad the arrays even if there are no images to make various
39
39
  # helping functionality (e.g. retrieving position for a name) easier.
40
40
  # I've also kept them in 6x6 here for better overview.
41
- PORTRAITS = [
42
- # http://eu.battle.net/sc2/static/local-common/images/sc2/portraits/0-75.jpg?v42
43
- ['Kachinsky', 'Cade', 'Thatcher', 'Hall', 'Tiger Marine', 'Panda Marine',
44
- 'General Warfield', 'Jim Raynor', 'Arcturus Mengsk', 'Sarah Kerrigan', 'Kate Lockwell', 'Rory Swann',
45
- 'Egon Stetmann', 'Hill', 'Adjutant', 'Dr. Ariel Hanson', 'Gabriel Tosh', 'Matt Horner',
46
- # Could not identify in order: Raynor in a Suit? Bullmarine? Nova?
47
- # Fiery Marine?
48
- 'Tychus Findlay', 'Zeratul', 'Valerian Mengsk', 'Spectre', '?', '?',
49
- '?', '?', 'SCV', 'Firebat', 'Vulture', 'Hellion',
50
- 'Medic', 'Spartan Company', 'Wraith', 'Diamondback', 'Probe', 'Scout'],
51
-
52
- # http://eu.battle.net/sc2/static/local-common/images/sc2/portraits/1-75.jpg?v42
53
- # Special Rewards - couldn't identify most of these.
54
- ['?', '?', '?', '?', '?', 'PanTerran Marine',
55
- '?', '?', '?', '?', '', '',
56
- '', '', '', '', '', '',
57
- '', '', '', '', '', '',
58
- '', '', '', '', '', '',
59
- '', '', '', '', '', ''],
60
-
61
- # http://eu.battle.net/sc2/static/local-common/images/sc2/portraits/2-75.jpg?v42
62
- ['Ghost', 'Thor', 'Battlecruiser', 'Nova', 'Zealot', 'Stalker',
63
- 'Phoenix', 'Immortal', 'Void Ray', 'Colossus', 'Carrier', 'Tassadar',
64
- 'Reaper', 'Sentry', 'Overseer', 'Viking', 'High Templar', 'Mutalisk',
65
- # Unidentified: Bird? Dog? Robot?
66
- 'Banshee', 'Hybrid Destroyer', 'Dark Voice', '?', '?', '?',
67
- # Unidentified: Worgen? Goblin? Chef?
68
- 'Orian', 'Wolf Marine', 'Murloc Marine', '?', '?', 'Zealot Chef',
69
- # Unidentified: KISS Marine? Dragon Marine? Dragon? Another Raynor?
70
- 'Stank', 'Ornatus', '?', '?', '?', '?'],
71
41
 
72
- # http://eu.battle.net/sc2/static/local-common/images/sc2/portraits/3-75.jpg?v42
73
- ['Urun', 'Nyon', 'Executor', 'Mohandar', 'Selendis', 'Artanis',
74
- 'Drone', 'Infested Colonist', 'Infested Marine', 'Corruptor', 'Aberration', 'Broodlord',
75
- 'Overmind', 'Leviathan', 'Overlord', 'Hydralisk Marine', "Zer'atai Dark Templar", 'Goliath',
76
- # Unidentified: Satan Marine?
77
- 'Lenassa Dark Templar', 'Mira Han', 'Archon', 'Hybrid Reaver', 'Predator', '?',
78
- 'Zergling', 'Roach', 'Baneling', 'Hydralisk', 'Queen', 'Infestor',
79
- 'Ultralisk', 'Queen of Blades', 'Marine', 'Marauder', 'Medivac', 'Siege Tank']
42
+ PORTRAITS = [
43
+ [
44
+ 'Kachinsky', 'Cade', 'Thatcher', 'Hall', 'Tiger Marine', 'Panda Marine',
45
+ 'General Warfield', 'Jim Raynor', 'Arcturus Mengsk', 'Sarah Kerrigan', 'Kate Lockwell', 'Rory Swann',
46
+ 'Egon Stetmann', 'Hill', 'Adjutant', 'Dr. Ariel Hanson', 'Gabriel Tosh', 'Matt Horner',
47
+ 'Tychus Findlay', 'Zeratul', 'Valerian Mengsk', 'Spectre', 'Raynor Marine', 'Tauren Marine',
48
+ 'Night Elf Banshee', 'Diablo Marine', 'SCV', 'Firebat', 'Vulture', 'Hellion',
49
+ 'Medic', 'Spartan Company', 'Wraith', 'Diamondback', 'Probe', 'Scout'
50
+ ],
51
+ [
52
+ 'Tauren Marine', 'Night Elf Banshee', 'Diablo Marine', 'Worgen Marine', 'Goblin Marine', 'PanTerran Marine',
53
+ 'Wizard Templar', 'Tyrael Marine', 'Witch Doctor Zergling', 'Stank', 'Night Elf Templar', 'Infested Orc',
54
+ '', 'Diablo Marine', '', 'Pandaren Firebat', 'Prince Valerian', 'Zagara',
55
+ 'Lasarra', 'Dehaka', 'Infested Stukov', 'Mira Horner', 'Primal Queen', 'Izsha',
56
+ 'Abathur', 'Ghost Kerrigan', 'Zurvan', 'Narud', '', '',
57
+ '', '', '', '', '', ''
58
+ ],
59
+ [
60
+ 'Ghost', 'Thor', 'Battlecruiser', 'Nova', 'Zealot', 'Stalker',
61
+ 'Phoenix', 'Immortal', 'Void Ray', 'Colossus', 'Carrier', 'Tassadar',
62
+ 'Reaper', 'Sentry', 'Overseer', 'Viking', 'High Templar', 'Mutalisk',
63
+ 'Banshee', 'Hybrid Destroyer', 'Dark Voice', 'Urubu', 'Lyote', 'Automaton 2000',
64
+ 'Orian', 'Wolf Marine', 'Murloc Marine', 'Worgen Marine', 'Goblin Marine', 'Zealot Chef',
65
+ 'Stank', 'Ornatus', 'Facebook Corps Members', 'Lion Marines', 'Dragons', 'Raynor Marine'
66
+ ],
67
+ [
68
+ 'Urun', 'Nyon', 'Executor', 'Mohandar', 'Selendis', 'Artanis',
69
+ 'Drone', 'Infested Colonist', 'Infested Marine', 'Corruptor', 'Aberration', 'Broodlord',
70
+ 'Overmind', 'Leviathan', 'Overlord', 'Hydralisk Marine', "Zer'atai Dark Templar", 'Goliath',
71
+ 'Lenassa Dark Templar', 'Mira Han', 'Archon', 'Hybrid Reaver', 'Predator', 'Unknown',
72
+ 'Zergling', 'Roach', 'Baneling', 'Hydralisk', 'Queen', 'Infestor',
73
+ 'Ultralisk', 'Queen of Blades', 'Marine', 'Marauder', 'Medivac', 'Siege Tank'
74
+ ],
75
+ [
76
+ 'Level 3 Zealot', 'Level 5 Stalker', 'Level 8 Sentinel', 'Level 11 Immortal', 'Level 14 Oracle', 'Level 17 High Templar',
77
+ 'Level 21 Tempest', 'Level 23 Colossus', 'Level 27 Carrier', 'Level 29 Zeratul', 'Level 3 Marine', 'Level 5 Marauder',
78
+ 'Level 8 Hellbat', 'Level 11 Widow Mine', 'Level 14 Medivac', 'Level 17 Banshee', 'Level 21 Ghost', 'Level 23 Thor',
79
+ 'Level 27 Battlecruiser', 'Level 29 Raynor', 'Level 3 Zergling', 'Level 5 Roach', 'Level 8 Hydralisk', 'Level 11 Locust',
80
+ 'Level 14 Swarm Host', 'Level 17 Infestor', 'Level 21 Viper', 'Level 23 Broodlord', 'Level 27 Ultralisk', 'Level 29 Kerrigan',
81
+ 'Protoss Champion',' Terran Champion', 'Zerg Champion', '', '', ''
82
+ ]
80
83
  ]
81
-
84
+
82
85
  # This is a convenience method that chains calls to ProfileScraper,
83
86
  # followed by a scrape of each league returned in the `leagues` array
84
87
  # in the profile_data. The end result is a fully scraped profile with
@@ -0,0 +1,24 @@
1
+ require 'date'
2
+ module BnetScraper
3
+ module Starcraft2
4
+ class Achievement
5
+ attr_accessor :title, :description
6
+ attr_reader :earned
7
+
8
+ def initialize options = {}
9
+ options.each_key do |key|
10
+ self.send "#{key}=", options[key]
11
+ end
12
+ end
13
+
14
+ def earned= date
15
+ @earned = convert_date date
16
+ end
17
+
18
+ def convert_date date
19
+ month, day, year = date.scan(/(\d+)\/(\d+)\/(\d+)/).first.map(&:to_i)
20
+ Date.new year, month, day
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,3 +1,5 @@
1
+ require 'bnet_scraper/starcraft2/achievement'
2
+
1
3
  module BnetScraper
2
4
  module Starcraft2
3
5
  # This pulls achievement information for an account. Note that currently only returns the overall achievements,
@@ -24,10 +26,11 @@ module BnetScraper
24
26
  # ],
25
27
  # progress: {
26
28
  # liberty_campaign: '1580',
27
- # exploration: '480',
29
+ # swarm_campaign: '480',
30
+ # matchmaking: '1100',
28
31
  # custom_game: '330',
29
- # cooperative: '660',
30
- # quick_match: '170'
32
+ # arcade: '660',
33
+ # exploration: '170'
31
34
  # }
32
35
  # }
33
36
  class AchievementScraper < BaseScraper
@@ -42,6 +45,8 @@ module BnetScraper
42
45
  end
43
46
 
44
47
  # retrieves the account's achievements overview page HTML for scraping
48
+ #
49
+ # @return [Nokogiri::HTML] The parsed HTML document
45
50
  def get_response
46
51
  response = Faraday.get "#{profile_url}achievements/"
47
52
  if response.success?
@@ -52,40 +57,59 @@ module BnetScraper
52
57
  end
53
58
 
54
59
  # scrapes the recent achievements from the account's achievements overview page
60
+ #
61
+ # @return [Array<Achievement>] Array of recent achievements
55
62
  def scrape_recent
56
63
  @recent = []
57
- 6.times do |num|
58
- achievement = {}
59
- div = response.css("#achv-recent-#{num}")
60
- if div
61
- achievement[:title] = div.css("div > div").inner_text.strip
62
- achievement[:description] = div.inner_text.gsub(achievement[:title], '').strip
63
- achievement[:earned] = response.css("#recent-achievements span")[(num*3)+1].inner_text
64
-
65
- @recent << achievement
66
- end
64
+ response.css(".recent-tile").size.times do |num|
65
+ achievement = extract_recent_achievement num
66
+ @recent.push(achievement) if achievement
67
67
  end
68
+
68
69
  @recent
69
70
  end
70
71
 
71
- # scrapes the progress of each achievement category from the account's achievements overview page
72
+ # Scrapes recent achievement by position in the sidebar
73
+ #
74
+ # @param [Fixnum] achievement position number, top-down
75
+ # @return [Achievement] achievement object containing all achievement information
76
+ def extract_recent_achievement num
77
+ if div = response.css("#achv-recent-#{num}")
78
+ Achievement.new({
79
+ title: div.children[1].inner_text,
80
+ description: div.children[2].inner_text.strip,
81
+ earned: response.css(".recent-tile")[num].css('span')[1].inner_text
82
+ })
83
+ end
84
+ end
85
+
86
+
87
+
88
+ # Scrapes the progress of each achievement category from the account's achievements
89
+ # overview page and returns them as a hash
90
+ #
91
+ # @return [Hash] Hash of achievement indicators broken down by category
72
92
  def scrape_progress
73
- progress_ach = response.css("#progress-module .achievements-progress:nth(2) span")
74
- @progress = {
75
- liberty_campaign: response.css(".progress-tile:nth-child(1) .profile-progress span").inner_text,
76
- exploration: response.css(".progress-tile:nth-child(2) .profile-progress span").inner_text,
77
- custom_game: response.css(".progress-tile:nth-child(3) .profile-progress span").inner_text,
78
- cooperative: response.css(".progress-tile:nth-child(4) .profile-progress span").inner_text,
79
- quick_match: response.css(".progress-tile:nth-child(5) .profile-progress span").inner_text,
80
- }
93
+ keys = [:liberty_campaign, :swarm_campaign, :matchmaking, :custom_game, :arcade, :exploration]
94
+ index = 1
95
+
96
+ @progress = keys.inject({}) do |hash, key|
97
+ hash[key] = response.css(".progress-tile:nth-child(#{index}) .profile-progress span").inner_text.to_i
98
+ index += 1
99
+
100
+ hash
101
+ end
81
102
  end
82
103
 
83
- # scrapes the showcase achievements from the account's achievements overview page
104
+ # Scrapes the showcase achievements from the account's achievements overview page
105
+ #
106
+ # @return [Array<Achievement>] Array containing all the showcased achievements
84
107
  def scrape_showcase
85
108
  @showcase = response.css("#showcase-module .progress-tile").map do |achievement|
86
- hsh = { title: achievement.css('.tooltip-title').inner_text.strip }
87
- hsh[:description] = achievement.css('div').inner_text.gsub(hsh[:title], '').strip
88
- hsh
109
+ Achievement.new({
110
+ title: achievement.css('.tooltip-title').inner_text.strip,
111
+ description: achievement.children[3].children[2].inner_text.strip
112
+ })
89
113
  end
90
114
  @showcase
91
115
  end
@@ -19,25 +19,41 @@ module BnetScraper
19
19
 
20
20
  def initialize options = {}
21
21
  if options[:url]
22
- extracted_data = options[:url].match(/http:\/\/(.+)\/sc2\/(.+)\/profile\/(.+)\/(\d{1})\/(.[^\/]+)\//)
23
- if extracted_data
24
- @region = REGION_DOMAINS[extracted_data[1]]
25
- @bnet_id = extracted_data[3]
26
- @bnet_index = extracted_data[4]
27
- @account = extracted_data[5]
28
- @url = options[:url]
29
- else
30
- raise BnetScraper::InvalidProfileError, "URL provided does not match Battle.net format"
31
- end
22
+ extract_data_from_url options[:url]
32
23
  elsif options[:bnet_id] && options[:account]
33
- @bnet_id = options[:bnet_id]
34
- @account = options[:account]
35
- @region = options[:region] || 'na'
36
- if options[:bnet_index]
37
- @bnet_index = options[:bnet_index]
38
- else
39
- set_bnet_index
40
- end
24
+ extract_data_from_options options
25
+ end
26
+ end
27
+
28
+ # Extracts information about the account from the URL string
29
+ #
30
+ # @param [String] url of the Battle.net profile page
31
+ def extract_data_from_url url
32
+ extracted_data = url.match(/http:\/\/(.+)\/sc2\/(.+)\/profile\/(.+)\/(\d{1})\/(.[^\/]+)\//)
33
+ if extracted_data
34
+ @region = REGION_DOMAINS[extracted_data[1]]
35
+ @language = extracted_data[2]
36
+ @bnet_id = extracted_data[3]
37
+ @bnet_index = extracted_data[4]
38
+ @account = extracted_data[5]
39
+ @url = url
40
+ else
41
+ raise BnetScraper::InvalidProfileError, "URL provided does not match Battle.net format"
42
+ end
43
+ end
44
+
45
+ # Extracts information about the account from an options hash
46
+ #
47
+ # @param [Hash] hash of Battle.net account infomation
48
+ def extract_data_from_options options
49
+ @bnet_id = options[:bnet_id]
50
+ @account = options[:account]
51
+ @region = options[:region] || 'na'
52
+
53
+ if options[:bnet_index]
54
+ @bnet_index = options[:bnet_index]
55
+ else
56
+ set_bnet_index
41
57
  end
42
58
  end
43
59
 
@@ -18,7 +18,7 @@ module BnetScraper
18
18
  # ]
19
19
  # }
20
20
  class MatchHistoryScraper < BaseScraper
21
- attr_reader :matches, :wins, :losses, :response
21
+ attr_reader :matches, :response
22
22
 
23
23
  # account's match history URL
24
24
  def match_url
@@ -39,24 +39,33 @@ module BnetScraper
39
39
  def scrape
40
40
  get_response
41
41
  @matches = []
42
- @wins = 0
43
- @losses = 0
44
42
 
45
43
  response.css('.match-row').each do |m|
46
- match = {}
47
- match = Match.new
48
-
49
- cells = m.css('td')
50
- match.map_name = cells[1].inner_text
51
- match.type = cells[2].inner_text
52
- match.outcome = (cells.css('.match-loss') ? :win : :loss)
53
- match.date = cells[4].inner_text.strip
54
-
55
- @matches << match
44
+ @matches.push extract_match_info m
56
45
  end
57
46
 
58
47
  @matches
59
48
  end
49
+
50
+ def wins
51
+ @wins ||= @matches.count { |m| m.outcome == :win }
52
+ end
53
+
54
+ def losses
55
+ @losses ||= @matches.count { |m| m.outcome == :loss }
56
+ end
57
+
58
+ def extract_match_info m
59
+ match = Match.new
60
+
61
+ cells = m.css('td')
62
+ match.map_name = cells[1].inner_text
63
+ match.type = cells[2].inner_text
64
+ match.outcome = (cells.css('.match-win')[0] ? :win : :loss)
65
+ match.date = cells[4].inner_text.strip
66
+
67
+ match
68
+ end
60
69
  end
61
70
  end
62
71
  end
@@ -5,7 +5,8 @@ module BnetScraper
5
5
  attr_accessor :portrait, :url, :achievement_points, :current_solo_league,
6
6
  :highest_solo_league, :current_team_league, :highest_team_league,
7
7
  :career_games, :games_this_season, :terran_swarm_level, :protoss_swarm_level,
8
- :zerg_swarm_level, :leagues, :swarm_levels
8
+ :zerg_swarm_level, :leagues, :swarm_levels, :terran_campaign_completion,
9
+ :zerg_campaign_completion
9
10
 
10
11
  def initialize options = {}
11
12
  options.each_key do |key|
@@ -40,6 +41,23 @@ module BnetScraper
40
41
  terran: @terran_swarm_level
41
42
  }
42
43
  end
44
+
45
+ def campaign_completion
46
+ {
47
+ terran: @terran_campaign_completion,
48
+ zerg: @zerg_campaign_completion
49
+ }
50
+ end
51
+
52
+ def completed_campaign campaign, difficulty = :normal
53
+ difficulties = [:unearned, :normal, :hard, :brutal]
54
+ ranking = campaign_completion[campaign]
55
+ if difficulties.index(ranking) >= difficulties.index(difficulty)
56
+ true
57
+ else
58
+ false
59
+ end
60
+ end
43
61
  end
44
62
  end
45
63
  end
@@ -42,84 +42,148 @@ module BnetScraper
42
42
  end
43
43
 
44
44
  def scrape
45
- get_profile_data
45
+ html = retrieve_data
46
+
47
+ get_profile_data html
48
+ get_portrait html
49
+ get_solo_league_info html
50
+ get_team_league_info html
51
+ get_swarm_levels html
52
+ get_campaign_completion html
46
53
  get_league_list
47
54
 
48
55
  @profile
49
56
  end
50
57
 
51
- # scrapes the profile page for basic account information
52
- def get_profile_data
58
+ # Retrieves the HTML document and feed into Nokogiri
59
+ #
60
+ # @return [Nokogiri::HTML] HTML document of Profile
61
+ def retrieve_data
53
62
  response = Faraday.get profile_url
54
63
 
55
64
  if response.success?
56
- html = Nokogiri::HTML(response.body)
57
-
58
- @profile.achievement_points = html.css("#profile-header h3").inner_html()
59
- @profile.career_games = html.css(".career-stat-block:nth-child(4) .stat-value").inner_html()
60
- @profile.games_this_season = html.css(".career-stat-block:nth-child(5) .stat-value").inner_html()
61
-
62
- get_portrait html
63
- get_solo_league_info html
64
- get_team_league_info html
65
- get_swarm_levels html
65
+ Nokogiri::HTML(response.body)
66
66
  else
67
67
  raise BnetScraper::InvalidProfileError
68
68
  end
69
69
  end
70
70
 
71
+ # Scrapes the Achievement Points, Career Games, and Games this Season from Profile
72
+ #
73
+ # @param [Nokogiri::HTML] Profile html object to scrape from
74
+ def get_profile_data html
75
+ @profile.achievement_points = html.css("#profile-header h3").inner_html()
76
+ @profile.career_games = html.css(".career-stat-block:nth-child(4) .stat-value").inner_html()
77
+ @profile.games_this_season = html.css(".career-stat-block:nth-child(5) .stat-value").inner_html()
78
+ end
79
+
80
+ # Extracts background spritesheet and sprite coordinates to map to a multidimensional
81
+ # array of portrait names. The first index is the spritesheet page, the second index
82
+ # is the position within the spritesheet
83
+ #
84
+ # @param [Nokogiri::XML] html node
85
+ # @return [String] Portrait name
71
86
  def get_portrait html
72
- # Portraits use spritemaps, so we extract positions and map to
73
- # PORTRAITS.
74
87
  @profile.portrait = begin
75
- portrait = html.css("#profile-header #portrait span").attr('style').to_s.scan(/url\('(.*?)'\) ([\-\d]+)px ([\-\d]+)px/).flatten
76
- portrait_map, portrait_size = portrait[0].scan(/(\d)\-(\d+)\.jpg/)[0]
77
- portrait_position = (((0-portrait[2].to_i) / portrait_size.to_i) * 6) + ((0-portrait[1].to_i) / portrait_size.to_i + 1)
78
- PORTRAITS[portrait_map.to_i][portrait_position-1]
88
+ portrait_info = extract_portrait_info html
89
+ position = get_portrait_position html, portrait_info
90
+ PORTRAITS[portrait_info[0].to_i][position-1]
79
91
  rescue
80
92
  nil
81
93
  end
82
94
  end
83
95
 
96
+ # Extracts portrait information (spritesheet page, portsize size, X, Y) from HTML page
97
+ #
98
+ # @param [Nokogiri::XML] html node
99
+ # @return [Fixnum, Fixnum, Fixnum, Fixnum] Array of sprite information
100
+ def extract_portrait_info html
101
+ html.css("#portrait .icon-frame").attr('style').to_s.scan(/url\('.+(\d+)-(\d+)\.jpg'\) ([\-\d]+)px ([\-\d]+)px/).flatten
102
+ end
103
+
104
+ # Translates x/y positions of the background spritesheet into an array index. There are
105
+ # 6 pictures per row, but X/Y is in pixels, so account for portrait size in the incrementor
106
+ #
107
+ # @param [Nokogiri::HTML] html node
108
+ # @param [Fixnum] size of portrait in pixels
109
+ # @return [Fixnum] index position of portrait spritesheet
110
+ def get_portrait_position html, portrait_info
111
+ size = portrait_info[1].to_i
112
+ x = portrait_info[2].to_i
113
+ y = portrait_info[3].to_i
114
+
115
+ ((-y/size) * 6) + (-x/size + 1)
116
+ end
117
+
118
+ # Extracts the current and highest ever solo league achieved
119
+ #
120
+ # @param [Nokogiri::XML] html node
84
121
  def get_solo_league_info html
85
- if html.css("#best-finish-SOLO div")[0]
86
- @profile.highest_solo_league = html.css("#best-finish-SOLO div")[0].children[2].inner_text.strip
87
- if html.css("#best-finish-SOLO div")[0].children[8]
88
- @profile.current_solo_league = html.css("#best-finish-SOLO div")[0].children[8].inner_text.strip
89
- else
90
- @profile.current_solo_league = "Not Yet Ranked"
91
- end
122
+ @profile.highest_solo_league = get_highest_league_info :solo, html
123
+ @profile.current_solo_league = get_current_league_info :solo, html
124
+ end
125
+
126
+ # Extracts the current and highest ever team league achieved
127
+ #
128
+ # @param [Nokogiri::XML] html node
129
+ def get_team_league_info html
130
+ @profile.highest_team_league = get_highest_league_info :team, html
131
+ @profile.current_team_league = get_current_league_info :team, html
132
+ end
133
+
134
+ # Extracts the highest league achieved for a given league type
135
+ #
136
+ # @param [Symbol] league to be scraped. Values: :solo or :team
137
+ # @param [Nokogiri::HTML] html ode to scrape
138
+ # @return [String] League of Ladder
139
+ def get_highest_league_info league_type, html
140
+ if div = html.css("#best-finish-#{league_type.upcase} div")[0]
141
+ div.children[2].inner_text.strip
92
142
  else
93
- @profile.highest_solo_league = "Not Yet Ranked"
94
- @profile.current_solo_league = "Not Yet Ranked"
143
+ "Not Yet Ranked"
95
144
  end
96
145
  end
97
146
 
98
- def get_team_league_info html
99
- if html.css("#best-finish-TEAM div")[0]
100
- @profile.highest_team_league = html.css("#best-finish-TEAM div")[0].children[2].inner_text.strip
101
- if html.css("#best-finish-TEAM div")[0].children[8]
102
- @profile.current_team_league = html.css("#best-finish-TEAM div")[0].children[8].inner_text.strip
103
- else
104
- @profile.current_team_league = "Not Yet Ranked"
105
- end
147
+ # Extracts the current league achieved for a given league type
148
+ #
149
+ # @param [Symbol] league to be scraped. Values: :solo or :team
150
+ # @param [Nokogiri::HTML] html node to scrape
151
+ # @return [String] League of Ladder
152
+ def get_current_league_info league_type, html
153
+ if div = html.css("#best-finish-#{league_type.upcase} div")[0].children[8]
154
+ div.inner_text.strip
106
155
  else
107
- @profile.highest_team_league = "Not Yet Ranked"
108
- @profile.current_team_league = "Not Yet Ranked"
156
+ "Not Yet Ranked"
109
157
  end
110
158
  end
111
159
 
160
+ # Extracts the HotS Swarm Levels for each race
161
+ #
162
+ # @param [Nokogiri::HTML] html node to scrape
112
163
  def get_swarm_levels html
113
164
  @profile.protoss_swarm_level = get_swarm_level :protoss, html
114
165
  @profile.terran_swarm_level = get_swarm_level :terran, html
115
166
  @profile.zerg_swarm_level = get_swarm_level :zerg, html
116
167
  end
117
168
 
169
+ # Extracts the swarm level for a given race
170
+ #
171
+ # @param [Symbol] race to determine the swarm level of
172
+ # @param [Nokogiri::HTML] html node to scrape from
173
+ # @return [Fixnum] Swarm Level
118
174
  def get_swarm_level race, html
119
175
  level = html.css(".race-level-block.#{race} .level-value").inner_html
120
176
  level.match(/Level (\d+)/)[1].to_i
121
177
  end
122
178
 
179
+ # Extracts completion level of the SC2 Campaigns
180
+ #
181
+ # @param [Nokogiri::HTML] html node to scrape
182
+ def get_campaign_completion html
183
+ @profile.terran_campaign_completion = html.css('.campaign-wings-of-liberty .badge')[0].attr('class').split[1].to_sym
184
+ @profile.zerg_campaign_completion = html.css('.campaign-heart-of-the-swarm .badge')[0].attr('class').split[1].to_sym
185
+ end
186
+
123
187
  # scrapes the league list from account's league page and sets an array of URLs
124
188
  def get_league_list
125
189
  response = Faraday.get profile_url + "ladder/leagues"