share_counts 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +32 -0
- data/README.rdoc +173 -0
- data/Rakefile +2 -0
- data/lib/share_counts/caching.rb +110 -0
- data/lib/share_counts/common.rb +106 -0
- data/lib/share_counts.rb +90 -0
- data/share_counts.gemspec +29 -0
- data/spec/share_count_spec.rb +3 -0
- data/spec/test_helper.rb +157 -0
- metadata +153 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
share_counts (0.0.1)
|
5
|
+
json
|
6
|
+
nokogiri
|
7
|
+
redis
|
8
|
+
rest-client
|
9
|
+
|
10
|
+
GEM
|
11
|
+
remote: http://rubygems.org/
|
12
|
+
specs:
|
13
|
+
ansi (1.2.2)
|
14
|
+
json (1.5.1)
|
15
|
+
mime-types (1.16)
|
16
|
+
minitest (2.0.2)
|
17
|
+
nokogiri (1.4.4)
|
18
|
+
redis (2.1.1)
|
19
|
+
rest-client (1.6.1)
|
20
|
+
mime-types (>= 1.16)
|
21
|
+
|
22
|
+
PLATFORMS
|
23
|
+
ruby
|
24
|
+
|
25
|
+
DEPENDENCIES
|
26
|
+
ansi
|
27
|
+
json
|
28
|
+
minitest
|
29
|
+
nokogiri
|
30
|
+
redis
|
31
|
+
rest-client
|
32
|
+
share_counts!
|
data/README.rdoc
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
= Share Counts
|
2
|
+
|
3
|
+
This gem makes it super easy to check how many times a page/URL has been shared on social networks and aggregators.
|
4
|
+
It also supports caching with Redis both to speed things up and to reduce the risk of problems with API rate limits.
|
5
|
+
|
6
|
+
Services currently supported:
|
7
|
+
|
8
|
+
* Reddit
|
9
|
+
* Digg
|
10
|
+
* Twitter
|
11
|
+
* Facebook (Share * Like)
|
12
|
+
* LinkedIn
|
13
|
+
* Google Buzz
|
14
|
+
* StumbleUpon
|
15
|
+
|
16
|
+
|
17
|
+
== Installation
|
18
|
+
|
19
|
+
Just..
|
20
|
+
gem install share_counts
|
21
|
+
|
22
|
+
and then require/bundle it.
|
23
|
+
|
24
|
+
== Basic Usage
|
25
|
+
|
26
|
+
You can check each service individually...
|
27
|
+
|
28
|
+
ruby-1.9.2-p0 :001 > require "share_counts"
|
29
|
+
=> true
|
30
|
+
|
31
|
+
ruby-1.9.2-p0 :002 > url = "http://vitobotta.com/awesomeprint-similar-production/"
|
32
|
+
=> "http://vitobotta.com/awesomeprint-similar-production/"
|
33
|
+
|
34
|
+
# Reddit
|
35
|
+
ruby-1.9.2-p0 :003 > ShareCounts.reddit url
|
36
|
+
=> 5
|
37
|
+
|
38
|
+
# Digg
|
39
|
+
ruby-1.9.2-p0 :004 > ShareCounts.digg url
|
40
|
+
=> 1
|
41
|
+
|
42
|
+
# Twitter
|
43
|
+
ruby-1.9.2-p0 :005 > ShareCounts.twitter url
|
44
|
+
=> 2
|
45
|
+
|
46
|
+
# Facebook shares
|
47
|
+
ruby-1.9.2-p0 :006 > ShareCounts.facebook url
|
48
|
+
=> 1
|
49
|
+
|
50
|
+
# Facebook likes
|
51
|
+
ruby-1.9.2-p0 :007 > ShareCounts.fblike url
|
52
|
+
=> 0
|
53
|
+
|
54
|
+
# LinkedIn
|
55
|
+
ruby-1.9.2-p0 :008 > ShareCounts.linkedin url
|
56
|
+
=> 2
|
57
|
+
|
58
|
+
# StumbleUpon
|
59
|
+
ruby-1.9.2-p0 :009 > ShareCounts.stumbleupon url
|
60
|
+
=> 0
|
61
|
+
|
62
|
+
# Google Buzz
|
63
|
+
ruby-1.9.2-p0 :010 > ShareCounts.googlebuzz url
|
64
|
+
=> 0
|
65
|
+
|
66
|
+
|
67
|
+
or you can get 'em all in one shot:
|
68
|
+
|
69
|
+
ruby-1.9.2-p0 :017 > ShareCounts.all "http://vitobotta.com/awesomeprint-similar-production/"
|
70
|
+
=> {:reddit=>5, :digg=>1, :twitter=>2, :facebook=>1, :fblike=>0, :linkedin=>2, :googlebuzz=>0, :stumbleupon=>0}
|
71
|
+
|
72
|
+
|
73
|
+
Additionally, for Facebook you can get shares and likes at once:
|
74
|
+
|
75
|
+
ruby-1.9.2-p0 :018 > ShareCounts.fball "http://vitobotta.com/awesomeprint-similar-production/"
|
76
|
+
=> {"share_count"=>1, "like_count"=>0}
|
77
|
+
|
78
|
+
|
79
|
+
You can also specify which networks you want to query at once:
|
80
|
+
|
81
|
+
ruby-1.9.2-p0 :004 > ShareCounts.selected "http://vitobotta.com/awesomeprint-similar-production/", [:reddit, :twitter]
|
82
|
+
=> {:reddit=>5, :twitter=>2}
|
83
|
+
|
84
|
+
Sometimes APIs may not be available or may be having issues (or there's some rate limit and you are making too many requests in a short time). When something goes wrong while querying a service, even for a max of 3 attempts, the share count for that service is left as set to nil. This way you can easily know whether a share count could be obtained or updated for a given URL, but simply checking if the share count is nil.
|
85
|
+
|
86
|
+
ruby-1.9.2-p0 :002 > ShareCounts.selected "http://vitobotta.com/awesomeprint-similar-production/", [:reddit, :twitter]
|
87
|
+
Making request to reddit...
|
88
|
+
Failed 1 attempt(s)
|
89
|
+
Failed 2 attempt(s)
|
90
|
+
Failed 3 attempt(s)
|
91
|
+
Something went wrong with reddit: can't convert nil into String
|
92
|
+
Making request to twitter...
|
93
|
+
=> {:reddit=>nil, :twitter=>2}
|
94
|
+
|
95
|
+
|
96
|
+
== Caching
|
97
|
+
|
98
|
+
Depending on how and in which kind of applications you may want to use this gem, if you make too many requests in a short time some APIs may fail because of rate limits. Plus, some API may respond too slowly at times or just be down (Digg seems to be the least reliable of the group so far!). So the gem also supports caching, at the moment with Redis only.
|
99
|
+
|
100
|
+
It's very easy to enable and use the caching:
|
101
|
+
|
102
|
+
ruby-1.9.2-p0 :002 > require 'benchmark'
|
103
|
+
=> true
|
104
|
+
|
105
|
+
# Enabling caching
|
106
|
+
ruby-1.9.2-p0 :003 > ShareCounts.use_cache
|
107
|
+
=> #<Redis client v2.1.1 connected to redis://127.0.0.1:6379/0 (Redis v2.0.3)>
|
108
|
+
|
109
|
+
|
110
|
+
# First run, values are not cached
|
111
|
+
ruby-1.9.2-p0 :004 > Benchmark.realtime { ShareCounts.all "http://vitobotta.com/awesomeprint-similar-production/" }
|
112
|
+
Making request to reddit...
|
113
|
+
Making request to digg...
|
114
|
+
Making request to twitter...
|
115
|
+
Making request to facebook...
|
116
|
+
Making request to fblike...
|
117
|
+
Making request to linkedin...
|
118
|
+
Making request to googlebuzz...
|
119
|
+
Making request to stumbleupon...
|
120
|
+
=> 3.7037899494171143
|
121
|
+
|
122
|
+
|
123
|
+
# Now values are cached
|
124
|
+
ruby-1.9.2-p0 :005 > Benchmark.realtime { ShareCounts.all "http://vitobotta.com/awesomeprint-similar-production/" }
|
125
|
+
Loaded reddit count from cache
|
126
|
+
Loaded digg count from cache
|
127
|
+
Loaded twitter count from cache
|
128
|
+
Loaded facebook count from cache
|
129
|
+
Loaded fblike count from cache
|
130
|
+
Loaded linkedin count from cache
|
131
|
+
Loaded googlebuzz count from cache
|
132
|
+
Loaded stumbleupon count from cache
|
133
|
+
=> 0.003225088119506836
|
134
|
+
|
135
|
+
|
136
|
+
By default, the gem connects to the Redis store listening on 127.0.0.1:6379, but you can override these settings when you enable the caching:
|
137
|
+
|
138
|
+
ruby-1.9.2-p0 :002 > ShareCounts.use_cache :host => "192.168.10.85", :port => 7500
|
139
|
+
=> #<Redis client v2.1.1 connected to redis://192.168.10.85:7500/0 (Redis v2.0.3)>
|
140
|
+
|
141
|
+
Or, if you are already using the "redis" gem in your application and therefore already have initialised a connection to the Redis store, you can either pass the reference to that instance:
|
142
|
+
|
143
|
+
ruby-1.9.2-p0 :003 > ShareCounts.use_cache :redis_store => YOUR_REFERENCE_TO_REDIS_STORE
|
144
|
+
=> #<Redis client v2.1.1 connected to redis://127.0.0.1:6379/0 (Redis v2.0.3)>
|
145
|
+
|
146
|
+
or just set the global variable $share_counts_cache to that reference.
|
147
|
+
|
148
|
+
Similarly, by default cached share counts expire in two minutes, but you can override this by setting the global variable $share_counts_cache_expire (in seconds).
|
149
|
+
|
150
|
+
You can also get an hash having all the cached URLs with their share counts:
|
151
|
+
|
152
|
+
ruby-1.9.2-p0 :009 > ShareCounts.cached
|
153
|
+
=> {"http://vitobotta.com/awesomeprint-similar-production/"=>{:fblike=>0, :stumbleupon=>0, :linkedin=>2, :googlebuzz=>0, :facebook=>1, :twitter=>2, :digg=>1, :reddit=>5}}
|
154
|
+
|
155
|
+
|
156
|
+
And, if needed, your can clear the cached values:
|
157
|
+
|
158
|
+
ruby-1.9.2-p0 :010 > ShareCounts.clear_cache
|
159
|
+
=> ["ShareCounts||fblike||http://vitobotta.com/awesomeprint-similar-production/", "ShareCounts||stumbleupon||http://vitobotta.com/awesomeprint-similar-production/", "ShareCounts||linkedin||http://vitobotta.com/awesomeprint-similar-production/", "ShareCounts||googlebuzz||http://vitobotta.com/awesomeprint-similar-production/", "ShareCounts||facebook||http://vitobotta.com/awesomeprint-similar-production/", "ShareCounts||twitter||http://vitobotta.com/awesomeprint-similar-production/", "ShareCounts||digg||http://vitobotta.com/awesomeprint-similar-production/", "ShareCounts||reddit||http://vitobotta.com/awesomeprint-similar-production/"]
|
160
|
+
ruby-1.9.2-p0 :011 > ShareCounts.cached
|
161
|
+
=> {
|
162
|
+
|
163
|
+
However the gem will namespace all the keys as you can see, in case you also use Redis for something else in the same app.
|
164
|
+
|
165
|
+
== TODO
|
166
|
+
|
167
|
+
* specs
|
168
|
+
|
169
|
+
|
170
|
+
== Authors
|
171
|
+
|
172
|
+
* Vito Botta ( http://vitobotta.com )
|
173
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
module ShareCountsCaching
|
2
|
+
|
3
|
+
#
|
4
|
+
#
|
5
|
+
# Returns true if the Redis
|
6
|
+
# cache store has been initialised
|
7
|
+
#
|
8
|
+
#
|
9
|
+
def cache_enabled?
|
10
|
+
!$share_counts_cache.nil?
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
#
|
16
|
+
#
|
17
|
+
# Removes from Redis cache store all the keys
|
18
|
+
# used by ShareCounts.
|
19
|
+
#
|
20
|
+
#
|
21
|
+
def clear_cache
|
22
|
+
($share_counts_cache || {}).keys.select{|cache_key| cache_key =~ /^ShareCounts/ }.each{|cache_key|
|
23
|
+
$share_counts_cache.del cache_key}
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
#
|
28
|
+
#
|
29
|
+
# Returns the cached share counts available for each URL, in the format
|
30
|
+
#
|
31
|
+
# {
|
32
|
+
# "URL 1": {
|
33
|
+
# :reddit => N,
|
34
|
+
# :digg => N,
|
35
|
+
# :twitter => N,
|
36
|
+
# :facebook => N,
|
37
|
+
# :fblike => N,
|
38
|
+
# :linkedin => N,
|
39
|
+
# :googlebuzz => N,
|
40
|
+
# :stumbleupon => N
|
41
|
+
# },
|
42
|
+
#
|
43
|
+
# "URL 2": {
|
44
|
+
# ...
|
45
|
+
# }
|
46
|
+
# }
|
47
|
+
#
|
48
|
+
#
|
49
|
+
def cached
|
50
|
+
urls = ($share_counts_cache || {}).keys.select{|k| k =~ /^ShareCounts/ }.inject({}) do |result, key|
|
51
|
+
data = key.split("||"); network = data[1]; url = data[2];
|
52
|
+
count = from_redis("ShareCounts||#{network}||#{url}")
|
53
|
+
(result[url] ||= {})[network.to_sym] = count unless ["all", "fball"].include? network
|
54
|
+
result
|
55
|
+
end
|
56
|
+
urls
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
#
|
61
|
+
# Enables caching with Redis.
|
62
|
+
#
|
63
|
+
# By default, it connects to 127.0.0.1:6379, but it is also
|
64
|
+
# possible to specify in the arguments :host, :port the
|
65
|
+
# connection details.
|
66
|
+
#
|
67
|
+
# If the application using this gem is already using Redis too,
|
68
|
+
# with the "redis" gem, it is possible to use the existing
|
69
|
+
# instance of Redis by either setting the :redist_store argument
|
70
|
+
# or by setting the global variable $share_counts_cache first.
|
71
|
+
#
|
72
|
+
#
|
73
|
+
def use_cache *args
|
74
|
+
arguments = args.inject({}) { |r, c| r.merge(c) }
|
75
|
+
$share_counts_cache ||= arguments[:redis_store] ||
|
76
|
+
Redis.new(:host => arguments[:host] || "127.0.0.1", :port => arguments[:port] || "6379")
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
#
|
83
|
+
#
|
84
|
+
# Caches the given value in Redis under the key specified.
|
85
|
+
# By default the value is cached for two minutes, but it
|
86
|
+
# is also possible to override this expiration time by
|
87
|
+
# setting the global variable $share_counts_cache_expire
|
88
|
+
# to a number of seconds.
|
89
|
+
#
|
90
|
+
#
|
91
|
+
def to_redis(cache_key, value)
|
92
|
+
$share_counts_cache.set cache_key, Marshal.dump(value)
|
93
|
+
$share_counts_cache.expire cache_key, $share_counts_cache_expire || 120
|
94
|
+
value
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
#
|
99
|
+
#
|
100
|
+
# Retrieves the value stores in Redis under
|
101
|
+
# the given key.
|
102
|
+
#
|
103
|
+
#
|
104
|
+
def from_redis(cache_key)
|
105
|
+
value = $share_counts_cache.get(cache_key)
|
106
|
+
return if value.nil?
|
107
|
+
Marshal.load value
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module ShareCountsCommon
|
2
|
+
|
3
|
+
private
|
4
|
+
|
5
|
+
#
|
6
|
+
#
|
7
|
+
# Given the name of one of the supported social networks and a URL,
|
8
|
+
# attempts the execution of the given block to fetch the relevant share count.
|
9
|
+
#
|
10
|
+
# If caching with Redis is enabled, it will first try to
|
11
|
+
# fetch the share count from cache instead, if there is a valid
|
12
|
+
# cached value for the combination of network/URL. When a share count is
|
13
|
+
# instead retrieved with an HTTP request to the network's API and
|
14
|
+
# the caching with Redis is enabled, the value fetched is also cached.
|
15
|
+
#
|
16
|
+
# NOTE: caching will be skipped if the block fails.
|
17
|
+
#
|
18
|
+
#
|
19
|
+
def try service, url, &block
|
20
|
+
cache_key = "ShareCounts||#{service}||#{url}"
|
21
|
+
if cache_enabled?
|
22
|
+
if result = from_redis(cache_key)
|
23
|
+
puts "Loaded #{service} count from cache"
|
24
|
+
result
|
25
|
+
else
|
26
|
+
puts "Making request to #{service}..."
|
27
|
+
to_redis(cache_key, yield)
|
28
|
+
end
|
29
|
+
else
|
30
|
+
puts "Redis caching is disabled - Making request to #{service}..."
|
31
|
+
yield
|
32
|
+
end
|
33
|
+
rescue Exception => e
|
34
|
+
puts "Something went wrong with #{service}: #{e}"
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
#
|
39
|
+
#
|
40
|
+
# Performs an HTTP request to the given API URL with the specified params
|
41
|
+
# and within 2 seconds, and max 3 attempts
|
42
|
+
#
|
43
|
+
# If a :callback param is also specified, then it is assumed that the API
|
44
|
+
# returns a JSON text wrapped in a call to a method by that callback name,
|
45
|
+
# therefore in this case it manipulates the response to extract only
|
46
|
+
# the JSON data required.
|
47
|
+
#
|
48
|
+
def make_request *args
|
49
|
+
result = nil
|
50
|
+
attempts = 1
|
51
|
+
|
52
|
+
begin
|
53
|
+
timeout(2) do
|
54
|
+
url = args.shift
|
55
|
+
params = args.inject({}) { |r, c| r.merge! c }
|
56
|
+
response = RestClient.get url, { :params => params }
|
57
|
+
|
58
|
+
|
59
|
+
# if a callback is specified, the expected response is in the format "callback_name(JSON data)";
|
60
|
+
# with the response ending with ";" and, in some cases, "\n"
|
61
|
+
result = params.keys.include?(:callback) \
|
62
|
+
? response.gsub(/^(.*);+\n*$/, "\\1").gsub(/^#{params[:callback]}\((.*)\)$/, "\\1") \
|
63
|
+
: response
|
64
|
+
end
|
65
|
+
|
66
|
+
rescue Exception => e
|
67
|
+
puts "Failed #{attempts} attempt(s)"
|
68
|
+
attempts += 1
|
69
|
+
retry if attempts <= 3
|
70
|
+
end
|
71
|
+
|
72
|
+
result
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
#
|
77
|
+
#
|
78
|
+
# Makes an HTTP request with the given URL and params, and assumes
|
79
|
+
# that the response is in JSON format, therefore it returns
|
80
|
+
# the parsed JSON.
|
81
|
+
#
|
82
|
+
#
|
83
|
+
def from_json *args
|
84
|
+
JSON.parse make_request *args
|
85
|
+
end
|
86
|
+
|
87
|
+
#
|
88
|
+
#
|
89
|
+
# Most social networks' APIs returns normal JSON data;
|
90
|
+
# this method simply extracts directly the share count from
|
91
|
+
# the given JSON data, by following a pattern common to the
|
92
|
+
# structure of most JSON responses.
|
93
|
+
#
|
94
|
+
# It also requires a :selector argument that determines how
|
95
|
+
# to "query" the JSON data in a way that emulates XPATH,
|
96
|
+
# so to extract the share count.
|
97
|
+
#
|
98
|
+
#
|
99
|
+
def extract_count *args
|
100
|
+
json = args.shift
|
101
|
+
result = args.first.flatten.last.split("/").inject( json.is_a?(Array) ? json.first : json ) {
|
102
|
+
|r, c| r[c].is_a?(Array) ? r[c].first : r[c]
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
data/lib/share_counts.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
%w(rest_client json nokogiri redis timeout).each{|x| require x}
|
2
|
+
|
3
|
+
require File.expand_path(File.dirname(__FILE__) + "/share_counts/common")
|
4
|
+
require File.expand_path(File.dirname(__FILE__) + "/share_counts/caching")
|
5
|
+
|
6
|
+
module ShareCounts
|
7
|
+
|
8
|
+
extend ShareCountsCommon
|
9
|
+
extend ShareCountsCaching
|
10
|
+
|
11
|
+
def self.supported_networks
|
12
|
+
%w(reddit digg twitter facebook fblike linkedin googlebuzz stumbleupon)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.reddit url
|
16
|
+
try("reddit", url) {
|
17
|
+
extract_count from_json( "http://www.reddit.com/api/info.json", :url => url ),
|
18
|
+
:selector => "data/children/data/score"
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.digg url
|
23
|
+
try("digg", url) {
|
24
|
+
extract_count from_json( "http://services.digg.com/2.0/story.getInfo", :links => url ),
|
25
|
+
:selector => "stories/diggs"
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.twitter url
|
30
|
+
try("twitter", url) {
|
31
|
+
extract_count from_json( "http://urls.api.twitter.com/1/urls/count.json", :url => url, :callback => "x" ),
|
32
|
+
:selector => "count"
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.facebook url
|
37
|
+
try("facebook", url) {
|
38
|
+
extract_count from_json("http://api.facebook.com/restserver.php", :v => "1.0", :method => "links.getStats",
|
39
|
+
:urls => url, :callback => "fb_sharepro_render", :format => "json" ), :selector => "share_count"
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.fblike url
|
44
|
+
try("fblike", url) {
|
45
|
+
extract_count from_json("http://api.facebook.com/restserver.php", :v => "1.0", :method => "links.getStats",
|
46
|
+
:urls => url, :callback => "fb_sharepro_render", :format => "json" ), :selector => "like_count"
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.fball url
|
51
|
+
try("fball", url) {
|
52
|
+
json = from_json("http://api.facebook.com/restserver.php", :v => "1.0",
|
53
|
+
:method => "links.getStats", :urls => url, :callback => "fb_sharepro_render", :format => "json"
|
54
|
+
).first.select{ |k,v| ["share_count", "like_count"].include? k }
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.linkedin url
|
59
|
+
try("linkedin", url) {
|
60
|
+
extract_count from_json("http://www.linkedin.com/cws/share-count",
|
61
|
+
:url => url, :callback => "IN.Tags.Share.handleCount" ), :selector => "count"
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.googlebuzz url
|
66
|
+
try("googlebuzz", url) {
|
67
|
+
from_json("http://www.google.com/buzz/api/buzzThis/buzzCounter",
|
68
|
+
:url => url, :callback => "google_buzz_set_count" )[url]
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.stumbleupon url
|
73
|
+
try("stumbleupon", url) {
|
74
|
+
Nokogiri::HTML.parse(
|
75
|
+
make_request("http://www.stumbleupon.com/badge/embed/5/", :url => url )
|
76
|
+
).xpath( "//body/div/ul/li[2]/a/span").text.to_i
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.all url
|
81
|
+
supported_networks.inject({}) { |r, c| r[c.to_sym] = ShareCounts.send(c, url); r }
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.selected url, selections
|
85
|
+
selections.map{|name| name.downcase}.select{|name| supported_networks.include? name.to_s}.inject({}) {
|
86
|
+
|r, c| r[c.to_sym] = ShareCounts.send(c, url); r }
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
$:.push File.expand_path("../lib/share_counts", __FILE__)
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "share_counts"
|
7
|
+
s.version = "0.0.2"
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Vito Botta"]
|
10
|
+
s.email = ["vito@botta.name"]
|
11
|
+
s.homepage = "https://github.com/vitobotta/share_counts"
|
12
|
+
s.summary = %q{The easiest way to check how many times a URL has been shared on Reddit, Digg, Twitter, Facebook, LinkedIn, GoogleBuzz and StumbleUpon!}
|
13
|
+
s.description = %q{The easiest way to check how many times a URL has been shared on Reddit, Digg, Twitter, Facebook, LinkedIn, GoogleBuzz and StumbleUpon!}
|
14
|
+
|
15
|
+
s.add_dependency "rest-client"
|
16
|
+
s.add_dependency "json"
|
17
|
+
s.add_dependency "nokogiri"
|
18
|
+
s.add_dependency "redis"
|
19
|
+
|
20
|
+
s.add_development_dependency "minitest"
|
21
|
+
s.add_development_dependency "ansi"
|
22
|
+
|
23
|
+
s.rubyforge_project = "share_counts"
|
24
|
+
|
25
|
+
s.files = `git ls-files`.split("\n")
|
26
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
27
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
28
|
+
s.require_paths = ["lib"]
|
29
|
+
end
|
data/spec/test_helper.rb
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem "minitest"
|
3
|
+
require 'minitest/autorun'
|
4
|
+
|
5
|
+
require 'ansi'
|
6
|
+
|
7
|
+
class MiniTest::Unit
|
8
|
+
include ANSI::Code
|
9
|
+
|
10
|
+
PADDING_SIZE = 4
|
11
|
+
|
12
|
+
def run(args = [])
|
13
|
+
@verbose = true
|
14
|
+
|
15
|
+
filter = if args.first =~ /^(-n|--name)$/ then
|
16
|
+
args.shift
|
17
|
+
arg = args.shift
|
18
|
+
arg =~ /\/(.*)\// ? Regexp.new($1) : arg
|
19
|
+
else
|
20
|
+
/./ # anything - ^test_ already filtered by #tests
|
21
|
+
end
|
22
|
+
|
23
|
+
@@out.puts "Loaded suite #{$0.sub(/\.rb$/, '')}\nStarted"
|
24
|
+
|
25
|
+
start = Time.now
|
26
|
+
run_test_suites filter
|
27
|
+
|
28
|
+
@@out.puts
|
29
|
+
@@out.puts "Finished in #{'%.6f' % (Time.now - start)} seconds."
|
30
|
+
|
31
|
+
@@out.puts
|
32
|
+
|
33
|
+
@@out.print "%d tests, " % test_count
|
34
|
+
@@out.print "%d assertions, " % assertion_count
|
35
|
+
@@out.print red { "%d failures, " % failures }
|
36
|
+
@@out.print yellow { "%d errors, " % errors }
|
37
|
+
@@out.puts cyan { "%d skips" % skips}
|
38
|
+
|
39
|
+
return failures + errors if @test_count > 0 # or return nil...
|
40
|
+
end
|
41
|
+
|
42
|
+
# Overwrite #run_test_suites so that it prints out reports
|
43
|
+
# as errors are generated.
|
44
|
+
def run_test_suites(filter = /./)
|
45
|
+
@test_count, @assertion_count = 0, 0
|
46
|
+
old_sync, @@out.sync = @@out.sync, true if @@out.respond_to? :sync=
|
47
|
+
|
48
|
+
TestCase.test_suites.each do |suite|
|
49
|
+
test_cases = suite.test_methods.grep(filter)
|
50
|
+
if test_cases.size > 0
|
51
|
+
@@out.print "\n#{suite}:\n"
|
52
|
+
end
|
53
|
+
|
54
|
+
test_cases.each do |test|
|
55
|
+
inst = suite.new test
|
56
|
+
inst._assertions = 0
|
57
|
+
|
58
|
+
t = Time.now
|
59
|
+
|
60
|
+
@broken = nil
|
61
|
+
|
62
|
+
@@out.print(case inst.run(self)
|
63
|
+
when :pass
|
64
|
+
@broken = false
|
65
|
+
green { pad_with_size "PASS" }
|
66
|
+
when :error
|
67
|
+
@broken = true
|
68
|
+
yellow { pad_with_size "ERROR" }
|
69
|
+
when :fail
|
70
|
+
@broken = true
|
71
|
+
red { pad_with_size "FAIL" }
|
72
|
+
when :skip
|
73
|
+
@broken = false
|
74
|
+
cyan { pad_with_size "SKIP" }
|
75
|
+
end)
|
76
|
+
|
77
|
+
|
78
|
+
# @@out.print " #{test.humanize.gsub(/Test\s\d+\s(.*)/,"\\1")} "
|
79
|
+
@@out.print " #{test} "
|
80
|
+
@@out.print " (%.2fs) " % (Time.now - t)
|
81
|
+
|
82
|
+
if @broken
|
83
|
+
@@out.puts
|
84
|
+
|
85
|
+
report = @report.last
|
86
|
+
@@out.puts pad(report[:message], 10)
|
87
|
+
trace = MiniTest::filter_backtrace(report[:exception].backtrace).first
|
88
|
+
@@out.print pad(trace, 10)
|
89
|
+
|
90
|
+
@@out.puts
|
91
|
+
end
|
92
|
+
|
93
|
+
@@out.puts
|
94
|
+
@test_count += 1
|
95
|
+
@assertion_count += inst._assertions
|
96
|
+
end
|
97
|
+
end
|
98
|
+
@@out.sync = old_sync if @@out.respond_to? :sync=
|
99
|
+
[@test_count, @assertion_count]
|
100
|
+
end
|
101
|
+
|
102
|
+
def pad(str, size=PADDING_SIZE)
|
103
|
+
" " * size + str
|
104
|
+
end
|
105
|
+
|
106
|
+
def pad_with_size(str)
|
107
|
+
pad("%5s" % str)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Overwrite #puke method so that is stores a hash
|
111
|
+
# with :message and :exception keys.
|
112
|
+
def puke(klass, meth, e)
|
113
|
+
result = nil
|
114
|
+
msg = case e
|
115
|
+
when MiniTest::Skip
|
116
|
+
@skips += 1
|
117
|
+
result = :skip
|
118
|
+
e.message
|
119
|
+
when MiniTest::Assertion
|
120
|
+
@failures += 1
|
121
|
+
result = :fail
|
122
|
+
e.message
|
123
|
+
else
|
124
|
+
@errors += 1
|
125
|
+
result = :error
|
126
|
+
"#{e.class}: #{e.message}\n"
|
127
|
+
end
|
128
|
+
|
129
|
+
@report << {:message => msg, :exception => e}
|
130
|
+
result
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
class TestCase
|
135
|
+
# Overwrite #run method so that is uses symbols
|
136
|
+
# as return values rather than characters.
|
137
|
+
def run(runner)
|
138
|
+
result = :pass
|
139
|
+
begin
|
140
|
+
@passed = nil
|
141
|
+
self.setup
|
142
|
+
self.send self.__name__
|
143
|
+
@passed = true
|
144
|
+
rescue Exception => e
|
145
|
+
@passed = false
|
146
|
+
result = runner.puke(self.class, self.__name__, e)
|
147
|
+
ensure
|
148
|
+
begin
|
149
|
+
self.teardown
|
150
|
+
rescue Exception => e
|
151
|
+
result = runner.puke(self.class, self.__name__, e)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
result
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
metadata
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: share_counts
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 2
|
9
|
+
version: 0.0.2
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Vito Botta
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-01-30 00:00:00 +00:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rest-client
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
type: :runtime
|
32
|
+
version_requirements: *id001
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: json
|
35
|
+
prerelease: false
|
36
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 0
|
43
|
+
version: "0"
|
44
|
+
type: :runtime
|
45
|
+
version_requirements: *id002
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: nokogiri
|
48
|
+
prerelease: false
|
49
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
segments:
|
55
|
+
- 0
|
56
|
+
version: "0"
|
57
|
+
type: :runtime
|
58
|
+
version_requirements: *id003
|
59
|
+
- !ruby/object:Gem::Dependency
|
60
|
+
name: redis
|
61
|
+
prerelease: false
|
62
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
segments:
|
68
|
+
- 0
|
69
|
+
version: "0"
|
70
|
+
type: :runtime
|
71
|
+
version_requirements: *id004
|
72
|
+
- !ruby/object:Gem::Dependency
|
73
|
+
name: minitest
|
74
|
+
prerelease: false
|
75
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
76
|
+
none: false
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
segments:
|
81
|
+
- 0
|
82
|
+
version: "0"
|
83
|
+
type: :development
|
84
|
+
version_requirements: *id005
|
85
|
+
- !ruby/object:Gem::Dependency
|
86
|
+
name: ansi
|
87
|
+
prerelease: false
|
88
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
segments:
|
94
|
+
- 0
|
95
|
+
version: "0"
|
96
|
+
type: :development
|
97
|
+
version_requirements: *id006
|
98
|
+
description: The easiest way to check how many times a URL has been shared on Reddit, Digg, Twitter, Facebook, LinkedIn, GoogleBuzz and StumbleUpon!
|
99
|
+
email:
|
100
|
+
- vito@botta.name
|
101
|
+
executables: []
|
102
|
+
|
103
|
+
extensions: []
|
104
|
+
|
105
|
+
extra_rdoc_files: []
|
106
|
+
|
107
|
+
files:
|
108
|
+
- .gitignore
|
109
|
+
- Gemfile
|
110
|
+
- Gemfile.lock
|
111
|
+
- README.rdoc
|
112
|
+
- Rakefile
|
113
|
+
- lib/share_counts.rb
|
114
|
+
- lib/share_counts/caching.rb
|
115
|
+
- lib/share_counts/common.rb
|
116
|
+
- share_counts.gemspec
|
117
|
+
- spec/share_count_spec.rb
|
118
|
+
- spec/test_helper.rb
|
119
|
+
has_rdoc: true
|
120
|
+
homepage: https://github.com/vitobotta/share_counts
|
121
|
+
licenses: []
|
122
|
+
|
123
|
+
post_install_message:
|
124
|
+
rdoc_options: []
|
125
|
+
|
126
|
+
require_paths:
|
127
|
+
- lib
|
128
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
segments:
|
134
|
+
- 0
|
135
|
+
version: "0"
|
136
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ">="
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
segments:
|
142
|
+
- 0
|
143
|
+
version: "0"
|
144
|
+
requirements: []
|
145
|
+
|
146
|
+
rubyforge_project: share_counts
|
147
|
+
rubygems_version: 1.3.7
|
148
|
+
signing_key:
|
149
|
+
specification_version: 3
|
150
|
+
summary: The easiest way to check how many times a URL has been shared on Reddit, Digg, Twitter, Facebook, LinkedIn, GoogleBuzz and StumbleUpon!
|
151
|
+
test_files:
|
152
|
+
- spec/share_count_spec.rb
|
153
|
+
- spec/test_helper.rb
|