quakelive_api 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +21 -0
- data/.travis.yml +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +76 -0
- data/Rakefile +14 -0
- data/lib/quakelive_api/base.rb +52 -0
- data/lib/quakelive_api/error/player_not_found.rb +6 -0
- data/lib/quakelive_api/error/request_error.rb +6 -0
- data/lib/quakelive_api/game_time.rb +34 -0
- data/lib/quakelive_api/items/award.rb +7 -0
- data/lib/quakelive_api/items/competitor.rb +7 -0
- data/lib/quakelive_api/items/favourite.rb +7 -0
- data/lib/quakelive_api/items/model.rb +7 -0
- data/lib/quakelive_api/items/recent_game.rb +7 -0
- data/lib/quakelive_api/items/record.rb +7 -0
- data/lib/quakelive_api/items/structurable.rb +13 -0
- data/lib/quakelive_api/items/weapon.rb +7 -0
- data/lib/quakelive_api/parser/awards.rb +50 -0
- data/lib/quakelive_api/parser/base.rb +33 -0
- data/lib/quakelive_api/parser/statistics.rb +81 -0
- data/lib/quakelive_api/parser/summary.rb +174 -0
- data/lib/quakelive_api/profile/awards/base.rb +24 -0
- data/lib/quakelive_api/profile/awards/career_milestones.rb +14 -0
- data/lib/quakelive_api/profile/awards/experience.rb +14 -0
- data/lib/quakelive_api/profile/awards/mad_skillz.rb +14 -0
- data/lib/quakelive_api/profile/awards/social_life.rb +15 -0
- data/lib/quakelive_api/profile/awards/sweet_success.rb +14 -0
- data/lib/quakelive_api/profile/statistics.rb +18 -0
- data/lib/quakelive_api/profile/summary.rb +35 -0
- data/lib/quakelive_api/profile.rb +43 -0
- data/lib/quakelive_api/version.rb +3 -0
- data/lib/quakelive_api.rb +38 -0
- data/quakelive_api.gemspec +28 -0
- data/test/fixtures/awards/career.txt +382 -0
- data/test/fixtures/awards/experience.txt +769 -0
- data/test/fixtures/awards/mad_skillz.txt +915 -0
- data/test/fixtures/awards/social_life.txt +155 -0
- data/test/fixtures/awards/sweet_success.txt +684 -0
- data/test/fixtures/profile/error.txt +24 -0
- data/test/fixtures/profile/not_found.txt +36 -0
- data/test/fixtures/profile/summary.txt +383 -0
- data/test/fixtures/statistics/emqz.txt +431 -0
- data/test/fixtures/statistics/xsi.txt +558 -0
- data/test/fixtures/summary/emqz.txt +304 -0
- data/test/fixtures/summary/mariano.txt +380 -0
- data/test/quakelive_api/game_time_test.rb +51 -0
- data/test/quakelive_api/profile/awards/career_milestones_test.rb +38 -0
- data/test/quakelive_api/profile/awards/experience_test.rb +38 -0
- data/test/quakelive_api/profile/awards/mad_skillz_test.rb +38 -0
- data/test/quakelive_api/profile/awards/social_life_test.rb +38 -0
- data/test/quakelive_api/profile/awards/sweet_success_test.rb +38 -0
- data/test/quakelive_api/profile/statistics_test.rb +75 -0
- data/test/quakelive_api/profile/summary_test.rb +97 -0
- data/test/quakelive_api/profile_test.rb +67 -0
- data/test/test_helper.rb +47 -0
- 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
data/Gemfile
ADDED
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,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,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
|