kickstapi 0.0.4 → 0.1.5
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 +4 -4
- data/README.md +18 -10
- data/lib/kickstapi/ghostly.rb +32 -0
- data/lib/kickstapi/kickstarter_gateway.rb +58 -0
- data/lib/kickstapi/project.rb +26 -4
- data/lib/kickstapi/project_mapper.rb +69 -0
- data/lib/kickstapi/version.rb +1 -1
- data/lib/kickstapi.rb +13 -62
- data/spec/kickstapi_spec.rb +44 -34
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e7a27eb2fdb171795742943d1dcdf6f7dcc13856
|
4
|
+
data.tar.gz: d2c7e82ac82d04e2399274f8dc5c2ad5c202e559
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 38c7392691d07678ade9e2400bb67a7d1c2fb1e422121f20457a774cf309f62757b510fef1e06daed6fc9230ead26ed8369f785b6ff654d433223bcb2929690c
|
7
|
+
data.tar.gz: f4bb5fb43a88e151a782f99fe780e3c05ce13d4e4ac33cf29df7e166cc1b41d95ce891e41c2ad592b3fec66928311799cf2f2108676fcc06aba4f68dd2101b6a
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Kickstapi [](https://travis-ci.org/kvannotten/kickstapi)
|
1
|
+
# Kickstapi [](https://travis-ci.org/kvannotten/kickstapi) [](http://badge.fury.io/rb/kickstapi)
|
2
2
|
|
3
3
|
This gem scrapes Kickstarter to create an API that facilitates the creation of applications querying Kickstarter.
|
4
4
|
|
@@ -24,20 +24,28 @@ Or install it yourself as:
|
|
24
24
|
require 'kickstapi'
|
25
25
|
|
26
26
|
# this gives you an array of Project objects containing 'Planetary Annihilation'
|
27
|
-
projects = Kickstapi.
|
27
|
+
projects = Kickstapi.find_projects_with_filter "Planetary Annihilation"
|
28
28
|
|
29
29
|
# lets take the first project
|
30
30
|
project = projects.first
|
31
31
|
|
32
32
|
# we can then perform the following methods on it
|
33
|
-
project.name
|
34
|
-
project.
|
35
|
-
project.
|
36
|
-
project.creator
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
project.
|
33
|
+
project.name # returns the name of the project
|
34
|
+
project.id # returns the id of the project
|
35
|
+
project.url # returns the url of the project
|
36
|
+
project.creator # returns the name of the creator
|
37
|
+
# The methods above are always available, the methods below
|
38
|
+
# will be lazy loaded when requested (this requires an additional
|
39
|
+
# HTML request)
|
40
|
+
project.about # returns the about section
|
41
|
+
project.pledged # returns the total amount pledged
|
42
|
+
project.percentage_funded # returns the percentage of funds that have been achieved so far
|
43
|
+
project.backers # returns the amount of backers
|
44
|
+
project.status # returns if the project is cancelled or succesful or still running
|
45
|
+
project.currency # returns the currency type (USD, CAD, GBP, EUR, ...)
|
46
|
+
project.goal # returns the fund goal
|
47
|
+
project.end_date # returns the date that the project (will) end(s)
|
48
|
+
project.hours_left # returns how many hours are still left on the project
|
41
49
|
|
42
50
|
```
|
43
51
|
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Kickstapi
|
2
|
+
module Ghostly
|
3
|
+
module Macros
|
4
|
+
private
|
5
|
+
def lazy_accessor(*names)
|
6
|
+
names.each do |name|
|
7
|
+
attr_writer name
|
8
|
+
define_method(name) do
|
9
|
+
load
|
10
|
+
instance_variable_get("@#{name}")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.included(other)
|
17
|
+
other.extend(Macros)
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_accessor :data_source
|
21
|
+
attr_writer :load_state
|
22
|
+
|
23
|
+
def load_state
|
24
|
+
@load_state ||= :ghost
|
25
|
+
end
|
26
|
+
|
27
|
+
def load
|
28
|
+
return if load_state == :loaded
|
29
|
+
data_source.load(self)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'mechanize'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Kickstapi
|
5
|
+
class KickstarterGateway
|
6
|
+
|
7
|
+
def projects_with_name(filter, offset = 1)
|
8
|
+
search_url = "http://www.kickstarter.com/projects/search?page=#{offset}utf8=%E2%9C%93&term=#{URI::encode filter}"
|
9
|
+
|
10
|
+
projects = []
|
11
|
+
should_page = true
|
12
|
+
|
13
|
+
agent.get(search_url) do |page|
|
14
|
+
page.search("div.project-card-wrap").each do |project|
|
15
|
+
p = Hash.new(0)
|
16
|
+
|
17
|
+
bb_card = project.search("h2.bbcard_name")
|
18
|
+
bb_card_link = bb_card.search("a").first
|
19
|
+
|
20
|
+
p[:name] = bb_card_link.content
|
21
|
+
p[:url] = "https://www.kickstarter.com#{bb_card_link.attributes["href"].value.split('?').first}"
|
22
|
+
p[:id] = JSON.parse(project.attributes["data-project"])["id"].to_i
|
23
|
+
p[:creator] = project.search(".bbcard_name span").text.gsub(/\n|by/, '')
|
24
|
+
|
25
|
+
projects << p
|
26
|
+
end
|
27
|
+
should_page = projects.count > 0
|
28
|
+
end
|
29
|
+
|
30
|
+
[projects, should_page]
|
31
|
+
end
|
32
|
+
|
33
|
+
def project_by_url(url)
|
34
|
+
project = {}
|
35
|
+
agent.get(url) do |page|
|
36
|
+
project[:name] = page.search(%Q{//h2[@class='mb1']//a}).text
|
37
|
+
project[:creator] = page.search(%Q{//span[@class='creator']//a}).text
|
38
|
+
project[:url] = url
|
39
|
+
project[:id] = page.search('div').select { |div| div[:class] =~ /^Project\d+/ }.map { |div| div[:class].to_s }.uniq.first.scan(/(\d+)/).flatten.first.to_i
|
40
|
+
project[:backers] = page.search(%Q{//data[@itemprop='Project[backers_count]']}).first.attributes["data-value"].value.to_i
|
41
|
+
project[:pledged] = page.search(%Q{//data[@itemprop='Project[pledged]']}).first.attributes["data-value"].value.to_f
|
42
|
+
project[:goal] = page.search(%Q{//div[@id='pledged']}).first.attributes['data-goal'].value.to_f
|
43
|
+
project[:currency] = page.search(%Q{//data[@itemprop='Project[pledged]']}).first.attributes["data-currency"].value
|
44
|
+
project[:percentage_funded] = page.search(%Q{//div[@id='pledged']}).first.attributes['data-percent-raised'].value.to_f * 100
|
45
|
+
project[:end_date] = DateTime.parse page.search(%Q{//span[@id='project_duration_data']}).first.attributes['data-end_time'].value
|
46
|
+
project[:hours_left] = page.search(%Q{//span[@id='project_duration_data']}).first.attributes['data-hours-remaining'].value.to_f
|
47
|
+
end
|
48
|
+
project
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def agent
|
54
|
+
@agent ||= Mechanize.new
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
data/lib/kickstapi/project.rb
CHANGED
@@ -1,15 +1,37 @@
|
|
1
1
|
require 'json'
|
2
|
+
require 'kickstapi/ghostly'
|
2
3
|
|
3
4
|
module Kickstapi
|
4
5
|
class Project
|
5
|
-
|
6
|
-
|
7
|
-
|
6
|
+
include Ghostly
|
7
|
+
|
8
|
+
attr_accessor :id, :name, :url, :creator
|
9
|
+
lazy_accessor :about, :pledged, :goal,
|
10
|
+
:currency, :percentage_funded, :backers,
|
11
|
+
:status, :end_date, :hours_left
|
8
12
|
|
13
|
+
def initialize(attributes = {})
|
14
|
+
complete(attributes)
|
15
|
+
end
|
16
|
+
|
17
|
+
def complete(attributes)
|
18
|
+
attributes.each do |key, value|
|
19
|
+
public_send("#{key}=", value)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
inspect
|
25
|
+
end
|
26
|
+
|
27
|
+
def ==(other)
|
28
|
+
other.is_a?(Project) && other.id == id
|
29
|
+
end
|
30
|
+
|
9
31
|
def to_hash
|
10
32
|
hash = {}
|
11
33
|
self.instance_variables.each do |var|
|
12
|
-
sym = var.to_s.gsub
|
34
|
+
sym = var.to_s.gsub(/@/, '')
|
13
35
|
hash[sym.to_sym] = self.instance_variable_get var
|
14
36
|
end
|
15
37
|
hash
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'kickstapi/project'
|
2
|
+
|
3
|
+
module Kickstapi
|
4
|
+
class ProjectMapper
|
5
|
+
attr_reader :gateway
|
6
|
+
|
7
|
+
def initialize(gateway)
|
8
|
+
@gateway = gateway
|
9
|
+
end
|
10
|
+
|
11
|
+
def projects_by_filter(filter, max_results = :all)
|
12
|
+
page = 1
|
13
|
+
should_page = true
|
14
|
+
projects = []
|
15
|
+
|
16
|
+
loop do
|
17
|
+
break unless should_page
|
18
|
+
break unless max_results == :all || max_results.to_i >= projects.size
|
19
|
+
|
20
|
+
projects_hashes, should_page = @gateway.projects_with_name(filter, page)
|
21
|
+
page += 1
|
22
|
+
|
23
|
+
projects_hashes.each do |project_hash|
|
24
|
+
project = Project.new(data_source: self)
|
25
|
+
|
26
|
+
project.complete(project_hash)
|
27
|
+
|
28
|
+
projects << project
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
projects = projects[0...max_results] unless max_results == :all
|
33
|
+
projects
|
34
|
+
end
|
35
|
+
|
36
|
+
def project_by_url(url)
|
37
|
+
project_hash = @gateway.project_by_url(url)
|
38
|
+
project = Project.new(data_source: self)
|
39
|
+
project.url = project_hash[:url]
|
40
|
+
|
41
|
+
fill_project(project, project_hash)
|
42
|
+
project
|
43
|
+
end
|
44
|
+
|
45
|
+
def load(project)
|
46
|
+
fill_project(project, @gateway.project_by_url(project.url))
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def fill_project(project, project_hash = {})
|
52
|
+
project.complete(project_hash)
|
53
|
+
project.load_state = :loaded
|
54
|
+
if project.pledged < project.goal
|
55
|
+
if project.end_date < DateTime.now
|
56
|
+
project.status = :failed
|
57
|
+
else
|
58
|
+
project.status = :running_not_yet_achieved
|
59
|
+
end
|
60
|
+
else
|
61
|
+
if project.end_date < DateTime.now
|
62
|
+
project.status = :succesful
|
63
|
+
else
|
64
|
+
project.status = :running_already_achieved
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/kickstapi/version.rb
CHANGED
data/lib/kickstapi.rb
CHANGED
@@ -1,74 +1,25 @@
|
|
1
1
|
require "kickstapi/version"
|
2
2
|
require "kickstapi/project"
|
3
|
+
require "kickstapi/kickstarter_gateway"
|
4
|
+
require "kickstapi/project_mapper"
|
5
|
+
require "kickstapi/ghostly"
|
3
6
|
|
4
7
|
require 'open-uri'
|
5
8
|
require 'mechanize'
|
6
9
|
|
7
10
|
module Kickstapi
|
8
|
-
def self.
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
def self.search_projects( filter = "", offset = 1)
|
13
|
-
search_url = "http://www.kickstarter.com/projects/search?page=#{offset}utf8=%E2%9C%93&term=#{URI::encode filter}"
|
11
|
+
def self.find_projects_with_filter(filter, max_results = :all)
|
12
|
+
gw = Kickstapi::KickstarterGateway.new
|
13
|
+
mapper = Kickstapi::ProjectMapper.new(gw)
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
page.search("div.project-card").each do |project|
|
18
|
-
p = Project.new
|
19
|
-
bb_card = project.search("h2.bbcard_name")
|
20
|
-
bb_card_link = bb_card.search("a").first
|
15
|
+
mapper.projects_by_filter filter, max_results
|
16
|
+
end
|
21
17
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
bb_card_author = bb_card.search("span").first
|
26
|
-
p.creator = bb_card_author.content.gsub(/\nby\n/, '').gsub(/\n/, '')
|
27
|
-
p.about = project.search("p.bbcard_blurb").first.content.strip
|
28
|
-
|
29
|
-
if project.search(".project-failed").first.nil? then
|
30
|
-
pledged = project.search("strong.project-pledged-amount").first.content.gsub(/\n/, '').gsub(/pledged/, '')
|
31
|
-
|
32
|
-
p.pledged = pledged[1, pledged.length].gsub(',', '').to_f
|
33
|
-
p.currency = pledged[0, 1]
|
34
|
-
p.percentage_funded = project.search("strong.project-pledged-percent").first.content.gsub(/\n/, '').gsub(/funded/, '')
|
35
|
-
p.backers = project.search("li.middle").first.search("strong").first.content.gsub(/\n/, '')
|
36
|
-
if project.search("li.ksr_page_timer").first.nil? then
|
37
|
-
p.status = project.search("li.last").first.search("strong").first.content
|
38
|
-
else
|
39
|
-
p.end_date = DateTime.parse(project.search("li.ksr_page_timer").first.attributes["data-end_time"].value)
|
40
|
-
p.status = "Running"
|
41
|
-
end
|
42
|
-
else
|
43
|
-
p.status = "Failed"
|
44
|
-
end
|
45
|
-
projects << p
|
46
|
-
end
|
47
|
-
end
|
18
|
+
def self.find_project_by_url(url)
|
19
|
+
gw = Kickstapi::KickstarterGateway.new
|
20
|
+
mapper = Kickstapi::ProjectMapper.new(gw)
|
48
21
|
|
49
|
-
|
50
|
-
end
|
51
|
-
|
52
|
-
def self.get_project name
|
53
|
-
return Kickstapi.search_projects(name).first
|
22
|
+
mapper.project_by_url url
|
54
23
|
end
|
55
|
-
|
56
|
-
def self.get_project_by_url url
|
57
|
-
p = Project.new
|
58
|
-
Kickstapi.get_agent.get(url) do |page|
|
59
|
-
p.name = page.search(%Q{//h2[@class='mb1']//a}).text
|
60
|
-
p.creator = page.search(%Q{//span[@class='creator']//a}).text
|
61
|
-
p.url = url
|
62
|
-
p.id = p.id = p.url.scan(/\/(\d+)\//).flatten.first.to_i
|
63
|
-
p.backers = page.search(%Q{//data[@itemprop='Project[backers_count]']}).first.attributes["data-value"].value.to_i
|
64
|
-
p.pledged = page.search(%Q{//data[@itemprop='Project[pledged]']}).first.attributes["data-value"].value.to_f
|
65
|
-
p.goal = page.search(%Q{//div[@id='pledged']}).first.attributes['data-goal'].value
|
66
|
-
p.currency = page.search(%Q{//data[@itemprop='Project[pledged]']}).first.attributes["data-currency"].value
|
67
|
-
p.percentage_funded = page.search(%Q{//div[@id='pledged']}).first.attributes['data-percent-raised'].value.to_f * 100
|
68
|
-
p.end_date = DateTime.parse page.search(%Q{//span[@id='project_duration_data']}).first.attributes['data-end_time'].value
|
69
|
-
end
|
70
|
-
|
71
|
-
p
|
72
|
-
end
|
73
|
-
|
24
|
+
|
74
25
|
end
|
data/spec/kickstapi_spec.rb
CHANGED
@@ -6,9 +6,8 @@ describe Kickstapi do
|
|
6
6
|
end
|
7
7
|
|
8
8
|
before :all do
|
9
|
-
@projects = Kickstapi.
|
10
|
-
@failure = Kickstapi.
|
11
|
-
@single_project = Kickstapi.get_project_by_url "http://www.kickstarter.com/projects/659943965/planetary-annihilation-a-next-generation-rts?ref=live"
|
9
|
+
@projects = Kickstapi.find_projects_with_filter "Planetary Annihilation" # succesful project
|
10
|
+
@failure = Kickstapi.find_projects_with_filter("Quala").first # failing project
|
12
11
|
end
|
13
12
|
|
14
13
|
context 'projects' do
|
@@ -24,44 +23,55 @@ describe Kickstapi do
|
|
24
23
|
|
25
24
|
its(:to_hash) { should be_kind_of Hash }
|
26
25
|
|
27
|
-
[ :id, :name, :url, :creator,
|
28
|
-
:about, :pledged, :currency,
|
29
|
-
:percentage_funded, :backers,
|
30
|
-
:status].each do |method|
|
31
|
-
it { should respond_to method }
|
32
|
-
its(method) { should_not be_nil }
|
33
|
-
its(:to_hash) { should have_key method }
|
34
|
-
end
|
35
|
-
|
36
26
|
its(:id) { should be_kind_of Fixnum }
|
37
|
-
its(:id) { should > 0 }
|
38
27
|
|
39
28
|
its(:pledged) { should be_kind_of Float }
|
40
29
|
|
41
30
|
it "returns valid json" do
|
42
|
-
expect { JSON.parse @projects.first.to_json }.
|
31
|
+
expect { JSON.parse @projects.first.to_json }.not_to raise_error
|
43
32
|
end
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
[ :id, :name, :url, :creator,
|
50
|
-
:pledged, :currency,
|
51
|
-
:percentage_funded, :backers,
|
52
|
-
:goal].each do |method|
|
53
|
-
it { should respond_to method }
|
54
|
-
its(method) { should_not be_nil }
|
55
|
-
its(:to_hash) { should have_key method }
|
33
|
+
|
34
|
+
it "should be in ghost state when first fetched" do
|
35
|
+
temp_project = Kickstapi.find_projects_with_filter("Ewe Topia").first
|
36
|
+
temp_project.load_state.should be_eql :ghost
|
56
37
|
end
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
38
|
+
|
39
|
+
it "should do an initial fetch of some fields, while in ghost state" do
|
40
|
+
temp_project = Kickstapi.find_projects_with_filter("Ewe Topia").first
|
41
|
+
temp_project.id
|
42
|
+
temp_project.creator
|
43
|
+
temp_project.url
|
44
|
+
temp_project.name
|
45
|
+
temp_project.load_state.should be_eql :ghost
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should lazily load all the arguments" do
|
49
|
+
temp_project = Kickstapi.find_projects_with_filter("Ewe Topia").first
|
50
|
+
temp_project.backers # fetch an item that is lazily loaded
|
51
|
+
temp_project.load_state.should be_eql :loaded
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should be retrievable from it's URL" do
|
55
|
+
temp_project = Kickstapi.find_project_by_url(@failure.url)
|
56
|
+
temp_project.name.should be_eql @failure.name
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should be comparable" do
|
60
|
+
temp_project = Kickstapi.find_project_by_url(@projects.first.url)
|
61
|
+
(temp_project == @projects.first).should be_true
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should be different from another project" do
|
65
|
+
(@projects.first == @failure).should be_false
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should mark a successful project" do
|
69
|
+
@projects.first.status.should be_eql :succesful
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should mark an unsuccessful project" do
|
73
|
+
@failure.status.should be_eql :failed
|
65
74
|
end
|
66
75
|
end
|
76
|
+
|
67
77
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kickstapi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kristof Vannotten
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-05-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -96,7 +96,10 @@ files:
|
|
96
96
|
- Rakefile
|
97
97
|
- kickstapi.gemspec
|
98
98
|
- lib/kickstapi.rb
|
99
|
+
- lib/kickstapi/ghostly.rb
|
100
|
+
- lib/kickstapi/kickstarter_gateway.rb
|
99
101
|
- lib/kickstapi/project.rb
|
102
|
+
- lib/kickstapi/project_mapper.rb
|
100
103
|
- lib/kickstapi/version.rb
|
101
104
|
- spec/kickstapi_spec.rb
|
102
105
|
- spec/spec_helper.rb
|
@@ -127,3 +130,4 @@ summary: This gem offers an API for Kickstarter
|
|
127
130
|
test_files:
|
128
131
|
- spec/kickstapi_spec.rb
|
129
132
|
- spec/spec_helper.rb
|
133
|
+
has_rdoc:
|