quakelive_api 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. data/.gitignore +21 -0
  2. data/.travis.yml +9 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +76 -0
  6. data/Rakefile +14 -0
  7. data/lib/quakelive_api/base.rb +52 -0
  8. data/lib/quakelive_api/error/player_not_found.rb +6 -0
  9. data/lib/quakelive_api/error/request_error.rb +6 -0
  10. data/lib/quakelive_api/game_time.rb +34 -0
  11. data/lib/quakelive_api/items/award.rb +7 -0
  12. data/lib/quakelive_api/items/competitor.rb +7 -0
  13. data/lib/quakelive_api/items/favourite.rb +7 -0
  14. data/lib/quakelive_api/items/model.rb +7 -0
  15. data/lib/quakelive_api/items/recent_game.rb +7 -0
  16. data/lib/quakelive_api/items/record.rb +7 -0
  17. data/lib/quakelive_api/items/structurable.rb +13 -0
  18. data/lib/quakelive_api/items/weapon.rb +7 -0
  19. data/lib/quakelive_api/parser/awards.rb +50 -0
  20. data/lib/quakelive_api/parser/base.rb +33 -0
  21. data/lib/quakelive_api/parser/statistics.rb +81 -0
  22. data/lib/quakelive_api/parser/summary.rb +174 -0
  23. data/lib/quakelive_api/profile/awards/base.rb +24 -0
  24. data/lib/quakelive_api/profile/awards/career_milestones.rb +14 -0
  25. data/lib/quakelive_api/profile/awards/experience.rb +14 -0
  26. data/lib/quakelive_api/profile/awards/mad_skillz.rb +14 -0
  27. data/lib/quakelive_api/profile/awards/social_life.rb +15 -0
  28. data/lib/quakelive_api/profile/awards/sweet_success.rb +14 -0
  29. data/lib/quakelive_api/profile/statistics.rb +18 -0
  30. data/lib/quakelive_api/profile/summary.rb +35 -0
  31. data/lib/quakelive_api/profile.rb +43 -0
  32. data/lib/quakelive_api/version.rb +3 -0
  33. data/lib/quakelive_api.rb +38 -0
  34. data/quakelive_api.gemspec +28 -0
  35. data/test/fixtures/awards/career.txt +382 -0
  36. data/test/fixtures/awards/experience.txt +769 -0
  37. data/test/fixtures/awards/mad_skillz.txt +915 -0
  38. data/test/fixtures/awards/social_life.txt +155 -0
  39. data/test/fixtures/awards/sweet_success.txt +684 -0
  40. data/test/fixtures/profile/error.txt +24 -0
  41. data/test/fixtures/profile/not_found.txt +36 -0
  42. data/test/fixtures/profile/summary.txt +383 -0
  43. data/test/fixtures/statistics/emqz.txt +431 -0
  44. data/test/fixtures/statistics/xsi.txt +558 -0
  45. data/test/fixtures/summary/emqz.txt +304 -0
  46. data/test/fixtures/summary/mariano.txt +380 -0
  47. data/test/quakelive_api/game_time_test.rb +51 -0
  48. data/test/quakelive_api/profile/awards/career_milestones_test.rb +38 -0
  49. data/test/quakelive_api/profile/awards/experience_test.rb +38 -0
  50. data/test/quakelive_api/profile/awards/mad_skillz_test.rb +38 -0
  51. data/test/quakelive_api/profile/awards/social_life_test.rb +38 -0
  52. data/test/quakelive_api/profile/awards/sweet_success_test.rb +38 -0
  53. data/test/quakelive_api/profile/statistics_test.rb +75 -0
  54. data/test/quakelive_api/profile/summary_test.rb +97 -0
  55. data/test/quakelive_api/profile_test.rb +67 -0
  56. data/test/test_helper.rb +47 -0
  57. metadata +220 -0
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .ruby-version
19
+ .rvmrc
20
+ .ruby-gemset
21
+ coverage/
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 1.9.2
5
+ - 1.9.3
6
+ - 2.0.0
7
+
8
+ notifications:
9
+ email: false
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in quakelive_api.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Rafal Wojsznis
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,76 @@
1
+ [![Code Climate](https://codeclimate.com/github/emq/quakelive_api.png)](https://codeclimate.com/github/emq/quakelive_api) [![Build Status](https://travis-ci.org/emq/quakelive_api.png?branch=master)](https://travis-ci.org/emq/quakelive_api)
2
+
3
+ # QuakeliveApi
4
+
5
+ This gem fetches (basic) publicly available data from [QuakeLive site][1]. Unfortunately currently there is no real API provided by ID so we're forced to parse html to get what we can. It will go down in flames if QL changes its internal html structure, so be prepared for that too.
6
+
7
+ The dirty job is made under `Parser` module (nothing pretty there, but response from QL is not pretty as well).
8
+
9
+ It is currently used in production environment, but consider it as work in progress for you own safety.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ gem 'quakelive_api'
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install quakelive_api
24
+
25
+ ## Usage
26
+
27
+ There is a main class called `QuakeliveApi::Profile` that will accept *player_name* (nickname from QL) as argument.
28
+ This class delegates its methods to other classes that *speaks* with quakelive.com. Each method call will execute a single query (using standard `Net::HTTP` Ruby library), parse the response using `Nokogiri` and returns the result (cached as instance variables).
29
+
30
+ Usage:
31
+
32
+ ``` ruby
33
+ profile = QuakeliveApi::Profile.new(player_name)
34
+
35
+ profile.summary # instance variable of QuakeliveApi::Profile::Summary
36
+ profile.statistics # instance variable of QuakeliveApi::Profile::Statistics
37
+ profile.awards_milestones # instance variable of QuakeliveApi::Profile::Awards::CareerMilestones
38
+ profile.awards_experience # instance variable of QuakeliveApi::Profile::Awards::Experience
39
+ profile.awards_skillz # instance variable of QuakeliveApi::Profile::Awards::MadSkillz
40
+ profile.awards_social # instance variable of QuakeliveApi::Profile::Awards::SocialLife
41
+ profile.awards_success # instance variable of QuakeliveApi::Profile::Awards::SweetSuccess
42
+
43
+ # there is also helper method called .each_award that will execute given block for all awards
44
+ # Note: those classes can be used separately (the initializer argument is the same)
45
+
46
+ # calling those methods may raise:
47
+ # - QuakeliveApi::Error::PlayerNotFound on invalid player_name
48
+ # - QuakeliveApi::Error::RequestError when ql site returns 'An Error Has Occurred' page
49
+ ```
50
+
51
+ ### Methods (should be self-explanatory)
52
+
53
+ - `QuakeliveApi::Profile::Summary`: country, nick, clan, model, memeber_since, last_game, time_played, wins, accuracy, losses, quits, frags, deaths, hits, shots, favourite, recent_awards, recent_games, recent_competitors
54
+
55
+ - `QuakeliveApi::Profile::Statistics` - weapons, records (each is array of _Struct_ objects)
56
+
57
+ - `QuakeliveApi::Profile::Awards` (each kind) - earned, unearned (same as above)
58
+
59
+ ## Known issues
60
+
61
+ - I noticed that sometimes QL site returns incomplete response for awards, that will trigger exception inside parser, unfortunately I can't quite narrow it down atm :(
62
+
63
+ ## TODO
64
+
65
+ - RDoc?
66
+ - it would be great to provide support for all stats like matches, competitors, friends etc.
67
+
68
+ ## Contributing
69
+
70
+ 1. Fork it
71
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
72
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
73
+ 4. Push to the branch (`git push origin my-new-feature`)
74
+ 5. Create new Pull Request
75
+
76
+ [1]: http://quakelive.com
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "test"
6
+ t.pattern = "test/**/*_test.rb"
7
+ end
8
+
9
+ desc "Open an irb session preloaded with this library"
10
+ task :console do
11
+ sh "irb -rubygems -I lib -r quakelive_api"
12
+ end
13
+
14
+ task default: :test
@@ -0,0 +1,52 @@
1
+ module QuakeliveApi
2
+ class Base
3
+ attr_accessor :player_name
4
+
5
+ def initialize(player_name)
6
+ @player_name = player_name
7
+
8
+ set_parser Nokogiri::HTML(get)
9
+ setup_variables!
10
+ end
11
+
12
+ def inspect
13
+ "#{self.class}:#{object_id}\n" + instance_variables.map do |v|
14
+ next if v.to_s == "@parser"
15
+ "#{v}=#{instance_variable_get(v).inspect}"
16
+ end.compact.join("\n")
17
+ end
18
+
19
+ private
20
+
21
+ def get
22
+ Net::HTTP.get(URI.parse(URI::encode("#{QuakeliveApi.site}#{url}")))
23
+ end
24
+
25
+ def url
26
+ raise NotImplementedError
27
+ end
28
+
29
+ def parser
30
+ @parser
31
+ end
32
+
33
+ def set_parser(document)
34
+ @parser ||= ::QuakeliveApi::Parser.const_get(self.class.class_name).new(document)
35
+ end
36
+
37
+ def self.class_name
38
+ name.split('::').last
39
+ end
40
+
41
+ def setup_variables!
42
+ raise Error::PlayerNotFound if parser.invalid_player?
43
+ raise Error::RequestError if parser.request_error?
44
+
45
+ setup_variables
46
+ end
47
+
48
+ def setup_variables
49
+ raise NotImplementedError
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,6 @@
1
+ module QuakeliveApi
2
+ module Error
3
+ class PlayerNotFound < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module QuakeliveApi
2
+ module Error
3
+ class RequestError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,34 @@
1
+ module QuakeliveApi
2
+ class GameTime
3
+ attr_accessor :ranked, :unranked
4
+
5
+ class Interval < Struct.new(:seconds, :minutes, :hours, :days)
6
+ def total
7
+ members.sum { |m| send(m).send(m) } # see what I did there?
8
+ end
9
+ end
10
+
11
+ # accepts unparsed string directly from QL profile, for example:
12
+ # * Ranked Time: 21:50 Unranked Time: 04:54
13
+ # * Ranked Time: 50.06:18:30 Unranked Time: 02:31:02
14
+ def initialize(unparsed_string)
15
+ matches = unparsed_string.match(/Ranked Time: ([\d:.]+) Unranked Time: ([\d:.]+)/)
16
+ return unless matches
17
+
18
+ @ranked = reverse_match(matches[1])
19
+ @unranked = reverse_match(matches[2])
20
+ end
21
+
22
+ def ==(other)
23
+ return ranked == other.ranked && unranked == other.unranked
24
+ end
25
+
26
+ private
27
+
28
+ def reverse_match(string)
29
+ attrs = string.split(/\.|:/).reverse.map { |a| a.to_i }
30
+ (4 - attrs.size).times { attrs << 0 }
31
+ Interval.new(*attrs)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,7 @@
1
+ module QuakeliveApi
2
+ module Items
3
+ class Award < Struct.new(:icon, :info, :name, :awarded, :description)
4
+ include Structurable
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module QuakeliveApi
2
+ module Items
3
+ class Competitor < Struct.new(:icon, :nick, :played)
4
+ include Structurable
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module QuakeliveApi
2
+ module Items
3
+ class Favourite < Struct.new(:arena, :game_type, :weapon)
4
+ include Structurable
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module QuakeliveApi
2
+ module Items
3
+ class Model < Struct.new(:name, :image)
4
+ include Structurable
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module QuakeliveApi
2
+ module Items
3
+ class RecentGame < Struct.new(:gametype, :finish, :played, :image)
4
+ include Structurable
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module QuakeliveApi
2
+ module Items
3
+ class Record < Struct.new(:title, :played, :finished, :wins, :quits, :completed, :wins_percentage)
4
+ include Structurable
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ module QuakeliveApi
2
+ module Items
3
+ module Structurable
4
+
5
+ def initialize(*args)
6
+ opts = args.last.is_a?(Hash) ? args.pop : {}
7
+ super(*args)
8
+ opts.each { |k, v| send("#{k}=", v) }
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ module QuakeliveApi
2
+ module Items
3
+ class Weapon < Struct.new(:name, :frags, :accuracy, :hits, :shots, :usage)
4
+ include Structurable
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,50 @@
1
+ module QuakeliveApi
2
+ module Parser
3
+ class Awards < Base
4
+
5
+ def earned
6
+ awards = document.css(selector(:earned))
7
+ if awards
8
+ awards.map { |node| parse_node(node) }
9
+ end
10
+ end
11
+
12
+ def unearned
13
+ document.css(selector(:unearned)).map { |node| parse_node(node) }
14
+ end
15
+
16
+ private
17
+
18
+ def selectors
19
+ {
20
+ :earned => ".detailArea",
21
+ :unearned => ".detailArea_off"
22
+ }
23
+ end
24
+
25
+ def parse_node(node)
26
+ attrs = {
27
+ :icon => node.at('img')['src'],
28
+ :info => node.at('img')['title'],
29
+ :name => node.at('span.bigRedTxt').content,
30
+ :description => node.at('span.blktxt_11').content,
31
+ :awarded => awarded_at(node)
32
+ }
33
+ Items::Award.new(attrs)
34
+ end
35
+
36
+ def awarded_at(node)
37
+ return unless node.css('ul.fl li').count >= 3
38
+
39
+ matches = node.at('ul.fl li:first-child').content.match(/(\d{2})\/(\d{2})\/(\d{4})/)
40
+ Date.new(matches[3].to_i, matches[1].to_i, matches[2].to_i) unless matches.nil?
41
+ end
42
+ end
43
+
44
+ class CareerMilestones < Awards; end
45
+ class Experience < Awards; end
46
+ class MadSkillz < Awards; end
47
+ class SocialLife < Awards; end
48
+ class SweetSuccess < Awards; end
49
+ end
50
+ end
@@ -0,0 +1,33 @@
1
+ module QuakeliveApi
2
+ module Parser
3
+ class Base
4
+ def initialize(document)
5
+ @document = document
6
+ end
7
+
8
+ def invalid_player?
9
+ document.css('.prf_header span').text =~ /Player not found/
10
+ end
11
+
12
+ def request_error?
13
+ document.css('#sorry_content p:first-child').text =~ /An error has occurred while handling your request/
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :document
19
+
20
+ def selectors
21
+ raise NotImplementedError
22
+ end
23
+
24
+ def selector(name)
25
+ selectors.fetch(name)
26
+ end
27
+
28
+ def to_integer(val)
29
+ val.gsub(',','').to_i
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,81 @@
1
+ module QuakeliveApi
2
+ module Parser
3
+ class Statistics < Base
4
+
5
+ def weapons
6
+ document.css(selector(:weapon)).each_with_index.map do |node, idx|
7
+ attrs = {
8
+ :name => node.text,
9
+ :frags => frags(weapon_next(:frags, idx)),
10
+ :accuracy => accuracy(weapon_next(:accuracy, idx)),
11
+ :usage => usage(weapon_next(:usage, idx))
12
+ }
13
+
14
+ hits, shots = hits_shots(weapon_next(:accuracy, idx))
15
+ attrs.merge!(:hits => hits, :shots => shots)
16
+
17
+ Items::Weapon.new(attrs)
18
+ end
19
+ end
20
+
21
+ def records
22
+ return if no_records?
23
+
24
+ document.css(selector(:record)).map do |node|
25
+
26
+ next if no_records?
27
+
28
+ attrs = {
29
+ :title => node.at('.col_st_gametype').text.strip,
30
+ :played => to_integer(node.at('.col_st_played').text),
31
+ :finished => to_integer(node.at('.col_st_finished').text),
32
+ :wins => to_integer(node.at('.col_st_wins').text),
33
+ :quits => to_integer(node.at('.col_st_withdraws').text),
34
+ :completed => to_integer(node.at('.col_st_completeperc').text.gsub('%','')),
35
+ :wins_percentage => to_integer(node.at('.col_st_winperc').text.gsub('%',''))
36
+ }
37
+ Items::Record.new(attrs)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def selectors
44
+ {
45
+ :weapon => ".prf_weapons .col_weapon",
46
+ :frags => ".col_frags",
47
+ :accuracy => ".col_accuracy",
48
+ :usage => ".col_usage",
49
+ :record => ".qlv_profile_section_statistics .prf_record > div"
50
+ }
51
+ end
52
+
53
+ def no_records?
54
+ document.at(selector(:record)).nil?
55
+ end
56
+
57
+ def hits_shots(node)
58
+ return [nil, nil] unless node['title']
59
+ res = node['title'].match(/Hits: ([\d,]+) Shots: ([\d,]+)/)
60
+ [res[1], res[2]].map { |r| to_integer r }
61
+ end
62
+
63
+ def usage(node)
64
+ to_integer node.text.gsub("%", '')
65
+ end
66
+
67
+ def weapon_next(css, index)
68
+ document.css(".prf_weapons #{selector(css)}")[index]
69
+ end
70
+
71
+ def frags(node)
72
+ to_integer node.text
73
+ end
74
+
75
+ def accuracy(node)
76
+ return if node.text == 'N/A'
77
+ to_integer node.at('span').text.gsub("%","")
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,174 @@
1
+ module QuakeliveApi
2
+ module Parser
3
+ class Summary < Base
4
+
5
+ def country
6
+ document.at(selector(:country))['title']
7
+ end
8
+
9
+ def nick
10
+ document.at(selector(:nick)).text
11
+ end
12
+
13
+ def clan
14
+ (c = document.at(selector(:clan))) ? c.text : nil
15
+ end
16
+
17
+ def model
18
+ div = document.at(selector(:model))
19
+ name = div['title']
20
+ image = decode_background(div['style'])
21
+ Items::Model.new(name, image)
22
+ end
23
+
24
+ def member_since
25
+ Date.parse vitals.at(selector(:member)).next.text.match(/([\w.\s,]+)/)[1]
26
+ end
27
+
28
+ def last_game
29
+ node = vitals.at selector(:last)
30
+ node ? decode_time(node['title']) : nil
31
+ end
32
+
33
+ def time_played
34
+ node = vitals.at selector(:played)
35
+ node ? GameTime.new(node['title']) : nil
36
+ end
37
+
38
+ def wins
39
+ to_integer vitals.at(selector(:wins)).next.text
40
+ end
41
+
42
+ def accuracy
43
+ vitals.at(selector(:accuracy)).next.text.match(/([\d.]+)%/)[1].to_f
44
+ end
45
+
46
+ def losses_quits
47
+ parse_slashed vitals.at(selector(:losses))
48
+ end
49
+
50
+ def frags_deaths
51
+ parse_slashed vitals.at(selector(:frags))
52
+ end
53
+
54
+ def hits_shots
55
+ parse_slashed vitals.at(selector(:hits))
56
+ end
57
+
58
+ def favourites
59
+ Items::Favourite.new(*document.css(selector(:favs))
60
+ .map { |n| n.next.text.strip }
61
+ .map { |n| n == "None" ? nil : n })
62
+ end
63
+
64
+ def awards
65
+ awards = document.css(selector(:awards)).map do |node|
66
+ title = node.at('.vcenter_data b')
67
+
68
+ next if title.text =~ /No recent award/
69
+
70
+ info = node['title']
71
+ icon = node.at('img')['src']
72
+ awarded = title.next.next
73
+ description = awarded.next.next
74
+
75
+ Items::Award.new(icon, info, title.text.strip, awarded.text.strip, description.text.strip.gsub("\n",""))
76
+ end.compact
77
+
78
+ awards.any? ? awards : nil
79
+ end
80
+
81
+ def recent_games
82
+ games = document.css(selector(:games)).map do |node|
83
+ gametype = decode_gametype node.at('img.gametype')['src']
84
+ finish = node.at('span.finish').text.strip.match(/Finish:\s+(\w+)/i)[1]
85
+ played = node.at('span.played').text.strip.match(/Played:\s+([\w ]+)/i)[1]
86
+ image = node.at('img.levelshot')['src']
87
+
88
+ Items::RecentGame.new(gametype, finish, played, image)
89
+ end.compact
90
+
91
+ games.any? ? games : nil
92
+ end
93
+
94
+ def recent_competitors
95
+ competitors = document.css(selector(:competitors)).map do |node|
96
+ next if node.at('.rcmp_none')
97
+
98
+ icon = decode_background node.at('.usericon_standard_lg')['style']
99
+ nick = node.at('a.player_nick_dark').text
100
+ played = decode_time(node.at('span.text_tooltip')['title'])
101
+
102
+ Items::Competitor.new(icon, nick, played )
103
+ end.compact
104
+
105
+ competitors.any? ? competitors : nil
106
+ end
107
+
108
+ private
109
+
110
+ def selectors
111
+ {
112
+ :country => ".playername img",
113
+ :nick => "#prf_player_name",
114
+ :clan => ".playername a.clan",
115
+ :model => ".prf_imagery div",
116
+ :vitals => ".prf_vitals p",
117
+ :member => "b:contains('Member Since')",
118
+ :last => "b:contains('Last Game') + span",
119
+ :played => "b:contains('Time Played') + span",
120
+ :wins => "b:contains('Wins')",
121
+ :losses => "b:contains('Losses')",
122
+ :frags => "b:contains('Frags')",
123
+ :hits => "b:contains('Hits')",
124
+ :accuracy => "b:contains('Accuracy')",
125
+ :favs => ".prf_faves b",
126
+ :awards => ".prf_awards .awd_details",
127
+ :games => ".recent_match",
128
+ :competitors => "#qlv_profileBottomInset .rcmp_block"
129
+ }
130
+ end
131
+
132
+ def decode_time(string)
133
+ Time.strptime(string, '%m/%d/%Y %H:%M %p')
134
+ end
135
+
136
+ # FIXME: not really fully implemented
137
+ def decode_gametype(string)
138
+ if string =~ /ca_/
139
+ 'CA'
140
+ elsif string =~ /tdm_/
141
+ 'TDM'
142
+ elsif string =~ /ctf_/
143
+ 'CTF'
144
+ elsif string =~ /duel_/
145
+ 'Duel'
146
+ elsif string =~ /ad_/
147
+ 'Attack&Defend'
148
+ elsif string =~ /ffa_/
149
+ 'FFA'
150
+ elsif string =~ /ft_/
151
+ 'FreezeTag'
152
+ elsif string =~ /race_/
153
+ 'Race'
154
+ elsif string =~ /rr_/
155
+ 'Red Rover'
156
+ end
157
+ end
158
+
159
+ def decode_background(string)
160
+ string.strip.match(/background(?:-image)?: url\(([\w:\/.]+)/)[1]
161
+ end
162
+
163
+ def vitals
164
+ document.at(selector(:vitals))
165
+ end
166
+
167
+ def parse_slashed(node)
168
+ match = node.next.text.match(/([\d,]+) \/ ([\d,]+)/)
169
+ [match[1], match[2]].map { |r| to_integer(r) }
170
+ end
171
+
172
+ end
173
+ end
174
+ end