xiv_lodestone 0.0.2
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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +80 -0
- data/Rakefile +6 -0
- data/lib/xiv_lodestone/lodestone_character.rb +114 -0
- data/lib/xiv_lodestone/lodestone_helper.rb +50 -0
- data/lib/xiv_lodestone/lodestone_parser.rb +155 -0
- data/lib/xiv_lodestone/version.rb +4 -0
- data/lib/xiv_lodestone.rb +8 -0
- data/spec/resources/character.html +6837 -0
- data/spec/resources/invalid.html +10 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/xiv_lodestone_spec.rb +111 -0
- data/xiv_lodestone.gemspec +25 -0
- metadata +108 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7a432c0ad6b311ba278eb7f9dd6b955f7e871a3e
|
4
|
+
data.tar.gz: 57ce133ac2807b8f95bb33e728753ccdb0541e21
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ddd77ff25fdc421423e8f47447e1e456adbb08cf0462c21cb74fe104b590aef64934d81b274fa544a919ef644ccc59632570fb05c7240aa691adb5ab3740da0b
|
7
|
+
data.tar.gz: acd1e309345ec4d6555b09771efafe865c510926a80bd355751e833e96d3939747bef716e8a4676178a35f81def9b23c374281b6a11b19ad03ef14f520d405d8
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 2.2.0
|
4
|
+
sudo: false
|
5
|
+
addons:
|
6
|
+
code_climate:
|
7
|
+
repo_token:
|
8
|
+
secure: "cNY3+gHTFC/mL3K34u+JR4UvsvREtytTUakNjy/J6avX1Zrgbzo26p4JzKNIsDzDvPkkaQqhLkdELMBnNjermTFFGtbJ8PNU1rNWwsLJ/R+RsFzrD8eBOgCBgLVt798qz7skLzKpiUzoU8rEq5ksyn1sBHo6OcrrKwvE8lQY8zs="
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Benjamin Evenson
|
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,80 @@
|
|
1
|
+
# XIVLodestone - A simple FFXIV lodestone scraper [](https://travis-ci.org/benjiro/XIV-lodestone) [](https://codeclimate.com/github/benjiro/XIV-lodestone) [](https://codeclimate.com/github/benjiro/XIV-lodestone)
|
2
|
+
|
3
|
+
A simple API for scraping information from FFXIV lodestone community website.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
- Character Search (via ID number or Name and Server)
|
7
|
+
- Character profile
|
8
|
+
- HP, MP, TP
|
9
|
+
- Race, Sex, Clan, Nameday, Guardian, City, Grand Company, Free Company
|
10
|
+
- Gear List - Shows item name, ilevel, slot, ffxiv db url, calculates total ilevel
|
11
|
+
- Disciple List(Classes) - Shows class name, level, current exp, total exp, icon url, calculates experience to next level
|
12
|
+
- All character attributes
|
13
|
+
|
14
|
+
## TODO
|
15
|
+
- Profile picture
|
16
|
+
- Achievements, Blogs, Friends list
|
17
|
+
- Free Company Search/Gather Member information
|
18
|
+
- Clean up smelly code 24/7
|
19
|
+
|
20
|
+
## Installation
|
21
|
+
|
22
|
+
Add this line to your application's Gemfile:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
gem 'xiv_lodestone'
|
26
|
+
```
|
27
|
+
|
28
|
+
And then execute:
|
29
|
+
|
30
|
+
$ bundle
|
31
|
+
|
32
|
+
Or install it yourself as:
|
33
|
+
|
34
|
+
$ gem install xiv_lodestone
|
35
|
+
|
36
|
+
## Usage
|
37
|
+
|
38
|
+
TODO: Write a more indepth guide
|
39
|
+
|
40
|
+
Once the gem is installed include it in your project.
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
require 'xiv_lodestone'
|
44
|
+
```
|
45
|
+
|
46
|
+
To parse a character
|
47
|
+
```ruby
|
48
|
+
$ character = XIVLodestone::Character("Benpi Kancho", "Tonberry")
|
49
|
+
$ character = XIVLodestone::Character(1549391)
|
50
|
+
```
|
51
|
+
|
52
|
+
Basic examples
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
character.gear.weapon # => Hash of below data
|
56
|
+
character.gear.weapon.name # => weapon name #String
|
57
|
+
character.gear.weapon.ilevel # => item level Integer
|
58
|
+
character.gear.weapon.slot # => item slot #String
|
59
|
+
character.gear.weapon.url # => url to ffxivdb #String
|
60
|
+
character.gear.ilevel # => Character ilevel
|
61
|
+
|
62
|
+
character.disciple.rogue # => Hash of below data
|
63
|
+
character.disciple.rogue.name # => class name #String
|
64
|
+
character.disciple.rogue.level # => current level #Integer
|
65
|
+
character.disciple.rogue.current_exp # => current experience #Interger
|
66
|
+
character.disciple.rogue.total_exp # => total experience #Integer
|
67
|
+
character.disciple.rogue.icon_url # => class icon url #String
|
68
|
+
character.disciple.rogue.next_level # => experience to required to level #Integer
|
69
|
+
|
70
|
+
character.str # => Character strength value
|
71
|
+
# More to come
|
72
|
+
```
|
73
|
+
|
74
|
+
## Contributing
|
75
|
+
|
76
|
+
1. Fork it ( https://github.com/[my-github-username]/xiv-lodestone/fork )
|
77
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
78
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
79
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
80
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'xiv_lodestone/lodestone_helper'
|
2
|
+
require 'xiv_lodestone/lodestone_parser'
|
3
|
+
|
4
|
+
module XIVLodestone
|
5
|
+
# A Object that representation a FFXIV:ARR character,
|
6
|
+
# all information is obtained from the lodestone website.
|
7
|
+
class Character
|
8
|
+
def initialize(*args)
|
9
|
+
parser = nil
|
10
|
+
if args.count == 1 && args.all? {|x| x.is_a? Fixnum}
|
11
|
+
parser = Parser.new(Helper.open_id(args.first))
|
12
|
+
elsif args.count == 2 && args.all? {|x| x.is_a? String}
|
13
|
+
parser = Parser.new(Helper.open_url(args.at(0), args.at(1)))
|
14
|
+
else
|
15
|
+
fail ArgumentError, "Invalid Arguments: player_id(Fixnum) or player_name(String), server_name(String)]"
|
16
|
+
end
|
17
|
+
|
18
|
+
@profile = Hash.new
|
19
|
+
@profile[:disciple] = DiscipleList.new(parser.get_classes)
|
20
|
+
@profile[:gear] = GearList.new(parser.get_gear)
|
21
|
+
@profile.merge!(parser.get_attributes)
|
22
|
+
@profile[:hp] = parser.get_hp
|
23
|
+
@profile[:mp] = parser.get_mp
|
24
|
+
@profile[:tp] = parser.get_tp
|
25
|
+
@profile[:sex] = parser.get_sex
|
26
|
+
@profile[:race] = parser.get_race
|
27
|
+
@profile[:clan] = parser.get_clan
|
28
|
+
@profile[:nameday] = parser.get_nameday
|
29
|
+
@profile[:guardian] = parser.get_guardian
|
30
|
+
@profile[:city] = parser.get_city
|
31
|
+
@profile[:grand_company] = parser.get_grand_company
|
32
|
+
@profile[:free_company] = parser.get_free_company
|
33
|
+
|
34
|
+
parser = nil #Close the reference so Nokogiri cleans up itself
|
35
|
+
end
|
36
|
+
|
37
|
+
def method_missing(method)
|
38
|
+
return @profile[method] if @profile.key?(method)
|
39
|
+
super
|
40
|
+
end
|
41
|
+
end
|
42
|
+
# A Object that represents a list of Gear pieces
|
43
|
+
# The initialiser takes a hash of items in the following layout
|
44
|
+
# { :weapon => ["Fist", 110, "Weapon", "http://...."], ... }
|
45
|
+
class GearList
|
46
|
+
def initialize(gear_list)
|
47
|
+
@list = Hash.new
|
48
|
+
gear_list.each do |key, value|
|
49
|
+
gear = Gear.new(value[0], value[1], value[2], value[3])
|
50
|
+
@list[key] = gear
|
51
|
+
end
|
52
|
+
end
|
53
|
+
# Calculates the total gear list ilevel
|
54
|
+
# Rounds to the nearest whole number like FFXIV ingame calculation
|
55
|
+
# returns a #Integer
|
56
|
+
def ilevel()
|
57
|
+
ilevel = 0
|
58
|
+
@list.each_value do |value|
|
59
|
+
ilevel += value.ilevel
|
60
|
+
end
|
61
|
+
(@list.key?(:shield)) ? (ilevel/13).round : (ilevel/12).round
|
62
|
+
end
|
63
|
+
# Generates access methods for each item slot
|
64
|
+
def method_missing(method)
|
65
|
+
return @list[method] if @list.key?(method)
|
66
|
+
super
|
67
|
+
end
|
68
|
+
# A object representation of a peacie of gear.
|
69
|
+
# TODO Add more information
|
70
|
+
class Gear
|
71
|
+
attr_reader :name, :ilevel, :slot, :url
|
72
|
+
|
73
|
+
def initialize(name, ilevel, slot, url)
|
74
|
+
@name = name
|
75
|
+
@ilevel = ilevel
|
76
|
+
@slot = slot
|
77
|
+
@url = url
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
# A object representation of disciples(classes)
|
82
|
+
# The initialiser takes a hash of Disciple, that layout follows
|
83
|
+
# { :rogue => ["Rogue", 1, 0, 300, "http://..."] }
|
84
|
+
class DiscipleList
|
85
|
+
def initialize(disciple_list)
|
86
|
+
@list = Hash.new
|
87
|
+
disciple_list.each do |key, value|
|
88
|
+
disciple = Disciple.new(value[0], value[1], value[2], value[3], value[4])
|
89
|
+
@list[key.to_sym] = disciple
|
90
|
+
end
|
91
|
+
# Generates access methods for each disciple slot
|
92
|
+
def method_missing(method)
|
93
|
+
return @list[method] if @list.key?(method)
|
94
|
+
super
|
95
|
+
end
|
96
|
+
end
|
97
|
+
# A object representation of a disciple
|
98
|
+
class Disciple
|
99
|
+
attr_reader :name, :level, :current_exp, :total_exp, :icon_url
|
100
|
+
|
101
|
+
def initialize(name, level, curr, req, icon)
|
102
|
+
@name = name
|
103
|
+
@level = level
|
104
|
+
@current_exp = curr
|
105
|
+
@total_exp = req
|
106
|
+
@icon_url = icon
|
107
|
+
end
|
108
|
+
# Returns the required experience to the next level
|
109
|
+
def next_level()
|
110
|
+
@total_exp - @current_exp
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'open-uri'
|
3
|
+
|
4
|
+
module XIVLodestone
|
5
|
+
# RuntimeError if a character can't be found
|
6
|
+
class CharacterNotFound < RuntimeError
|
7
|
+
end
|
8
|
+
# RuntimeError if more than one character is found
|
9
|
+
class MoreThanOneCharacter < RuntimeError
|
10
|
+
end
|
11
|
+
# A class of helper methods
|
12
|
+
class Helper
|
13
|
+
# Find a character profile from a given name and
|
14
|
+
# server. Returns a Nokogiri XML document of the
|
15
|
+
# characters page.
|
16
|
+
def self.open_url(name, server)
|
17
|
+
page = Nokogiri::HTML(open_character_url(name, server))
|
18
|
+
check_valid_url(page)
|
19
|
+
id = page.xpath('//h4/a')[0]['href'].split(/\//).last
|
20
|
+
Nokogiri::HTML(open_id_url(id))
|
21
|
+
end
|
22
|
+
# Find a characters profiles from a given id.
|
23
|
+
# Returns a Nokogiri XML document of the characters
|
24
|
+
# page.
|
25
|
+
def self.open_id(id)
|
26
|
+
Nokogiri::HTML(open_id_url(id))
|
27
|
+
end
|
28
|
+
# Open a URL with the given name and server.
|
29
|
+
# Returns a file stream.
|
30
|
+
def self.open_character_url(name, server)
|
31
|
+
open('http://na.finalfantasyxiv.com/lodestone/character/' \
|
32
|
+
"?q=#{name}&worldname=#{server}")
|
33
|
+
end
|
34
|
+
# Open a URL with the given id.
|
35
|
+
# Reutrn a file stream.
|
36
|
+
def self.open_id_url(id)
|
37
|
+
open("http://na.finalfantasyxiv.com/lodestone/character/#{id}")
|
38
|
+
end
|
39
|
+
# Validates a Nokogiri document for a character.
|
40
|
+
# Throws a CharacterNotFound exception.
|
41
|
+
# Throws a MoreThanOneCharacter exception.
|
42
|
+
def self.check_valid_url(page)
|
43
|
+
fail CharacterNotFound unless page.at_xpath('//h4/a')
|
44
|
+
fail MoreThanOneCharacter if page.xpath('//h4/a').size > 1
|
45
|
+
end
|
46
|
+
|
47
|
+
private_class_method :open_character_url, :open_id_url
|
48
|
+
private_class_method :check_valid_url
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module XIVLodestone
|
4
|
+
class Parser
|
5
|
+
# An exception for a invalid Nokogiri page
|
6
|
+
class InvalidDocument < Exception
|
7
|
+
end
|
8
|
+
# default constructor, initalises @page with a
|
9
|
+
# Nokogiri document. Raises a InvalidDocument eception
|
10
|
+
# if page is invalid
|
11
|
+
def initialize(page)
|
12
|
+
fail InvalidDocument, 'Invalid page' if page.nil?
|
13
|
+
@page = page
|
14
|
+
end
|
15
|
+
# Returns a #Hash of the characters class levels
|
16
|
+
# Example { :Gladitor => 10, ... }
|
17
|
+
# If no classes found returns a empty #Hash
|
18
|
+
def get_classes()
|
19
|
+
class_list = Hash.new
|
20
|
+
@page.xpath('//table[@class="class_list"]/tr/td').each_slice(3) do |td|
|
21
|
+
# Not a valid class
|
22
|
+
next if td[0].text.empty?
|
23
|
+
|
24
|
+
name = td[0].text
|
25
|
+
level = td[1].text.to_i
|
26
|
+
exp = td[2].text.split(/\//)
|
27
|
+
icon = td[0].at_css('img')['src']
|
28
|
+
|
29
|
+
class_list[name.downcase.to_sym] = [name, level, exp[0].to_i,
|
30
|
+
exp[1].to_i, icon]
|
31
|
+
end
|
32
|
+
class_list
|
33
|
+
end
|
34
|
+
# Returns a #Hash of the character attributes
|
35
|
+
# Example { :str => 243, ... }
|
36
|
+
# If no attributes found returns a empty #Hash
|
37
|
+
def get_attributes()
|
38
|
+
stats = Hash.new
|
39
|
+
@page.xpath('//div[starts-with(@class, "param_left_area_inner")]/ul/li').each_with_index do |li, index|
|
40
|
+
if index < 6
|
41
|
+
stats[replace_downcase(li['class']).to_sym] = li.text.to_i
|
42
|
+
else
|
43
|
+
ele = li.text.split(/(?<=\D)(?=\d)/)
|
44
|
+
stats[replace_downcase(ele[0]).to_sym] = ele[1].to_i
|
45
|
+
end
|
46
|
+
end
|
47
|
+
stats
|
48
|
+
end
|
49
|
+
# Returns a #Hash of the characters gear list
|
50
|
+
# Example { :head => [ "item_name", "item_url" ], ... }
|
51
|
+
# If no gear found returns a empty #Hash
|
52
|
+
def get_gear()
|
53
|
+
# TODO: Smelly code, rewrite
|
54
|
+
items = Hash.new
|
55
|
+
ring_count = 1
|
56
|
+
@page.xpath("(//div[@class='item_detail_box'])[position() < 13]").each_with_index do |item, index|
|
57
|
+
type = get_item_type(item.at_css('h3.category_name').text)
|
58
|
+
level = item.xpath('//div[@class="pt3 pb3"]')[index].text.split(/ /).last.to_i
|
59
|
+
if type.eql? "ring"
|
60
|
+
items["#{type}#{ring_count}".to_sym] = [item.css('h2').text, level, type, "http://na.finalfantasyxiv.com#{item.css('a')[0]['href']}"]
|
61
|
+
ring_count += 1
|
62
|
+
else
|
63
|
+
items[replace_downcase(type).to_sym] = [item.css('h2').text, level, type, "http://na.finalfantasyxiv.com#{item.css('a')[0]['href']}"]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
items
|
67
|
+
end
|
68
|
+
# Returns a string representing what item it is
|
69
|
+
def get_item_type(item_name)
|
70
|
+
if item_name =~ /(Arm|Arms|Grimoire|Primary Tool)/i
|
71
|
+
return "weapon"
|
72
|
+
elsif item_name =~ /Shield/i
|
73
|
+
return "shield"
|
74
|
+
else
|
75
|
+
return item_name.downcase
|
76
|
+
end
|
77
|
+
end
|
78
|
+
# Returns a #Integer of the characters hp
|
79
|
+
# otherwise returns nil
|
80
|
+
def get_hp()
|
81
|
+
hp = @page.at_xpath('//li[@class="hp"]')
|
82
|
+
hp.nil? ? nil : hp.text.to_i
|
83
|
+
end
|
84
|
+
# Returns a #Integer of the characters mp
|
85
|
+
# otherwise returns nil
|
86
|
+
def get_mp()
|
87
|
+
mp = @page.at_xpath('//li[@class="mp"]')
|
88
|
+
mp.nil? ? nil : mp.text.to_i
|
89
|
+
end
|
90
|
+
# Returns a #Integer of the characters tp
|
91
|
+
# otherwise returns nil
|
92
|
+
def get_tp()
|
93
|
+
tp = @page.at_xpath('//li[@class="tp"]')
|
94
|
+
tp.nil? ? nil : tp.text.to_i
|
95
|
+
end
|
96
|
+
# Returns a #String of the characters sex
|
97
|
+
# example "Male" or "Female"
|
98
|
+
# otherwise returns nil
|
99
|
+
def get_sex()
|
100
|
+
sex = @page.at_xpath('//div[@class="chara_profile_title"]')
|
101
|
+
if sex.nil?
|
102
|
+
nil
|
103
|
+
else
|
104
|
+
sex.text.split(/\//).last =~ /♂/i ? "Male" : "Female"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
# Returns a #String of the characters race
|
108
|
+
# otherwise returns nil
|
109
|
+
def get_race()
|
110
|
+
race = @page.at_xpath('//div[@class="chara_profile_title"]')
|
111
|
+
race.nil? ? nil : race.text.split(/\//).first.strip!
|
112
|
+
end
|
113
|
+
# Returns a #String of the characters clan
|
114
|
+
# otherwise returns nil
|
115
|
+
def get_clan()
|
116
|
+
clan = @page.at_xpath('//div[@class="chara_profile_title"]')
|
117
|
+
clan.nil? ? nil : clan.text.split(/\//)[1].strip!
|
118
|
+
end
|
119
|
+
# Returns a #String with the nameday
|
120
|
+
# otherwise returns nil
|
121
|
+
def get_nameday()
|
122
|
+
nameday = @page.at_xpath('(//div[@class="chara_profile_table"]/dl/dd)[1]')
|
123
|
+
nameday.nil? ? nil : nameday.text
|
124
|
+
end
|
125
|
+
# Returns a #String with the guardian name
|
126
|
+
# otherwise returns nil
|
127
|
+
def get_guardian()
|
128
|
+
guardian = @page.at_xpath('(//div[@class="chara_profile_table"]/dl/dd)[2]')
|
129
|
+
guardian.nil? ? nil : guardian.text
|
130
|
+
end
|
131
|
+
# Returns a #String the city name
|
132
|
+
# otherwise returns nil
|
133
|
+
def get_city()
|
134
|
+
city = @page.at_xpath('(//dd[@class="txt_name"])[1]')
|
135
|
+
city.nil? ? nil : city.text
|
136
|
+
end
|
137
|
+
# Returns a #String with the grandcompany
|
138
|
+
# otherwise returns nil
|
139
|
+
def get_grand_company()
|
140
|
+
company = @page.at_xpath('(//dd[@class="txt_name"])[2]')
|
141
|
+
company.nil? ? nil : company.text
|
142
|
+
end
|
143
|
+
# Returns a #Array with the freecompany name and url
|
144
|
+
# otherwise returns nil
|
145
|
+
def get_free_company()
|
146
|
+
element = @page.at_xpath('//dd[@class="txt_name"]/a')
|
147
|
+
element.nil? ? nil : [ element.text, "http://na.finalfantasyxiv.com#{element['href']}" ]
|
148
|
+
end
|
149
|
+
# Replaces spaces wtih underscores, and downcases
|
150
|
+
# Returns a #String
|
151
|
+
def replace_downcase(string)
|
152
|
+
string.gsub(" ", "_").downcase
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|