bnet_scraper 0.4.0 → 0.5.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.
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"