peekapp 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +20 -0
- data/README.markdown +42 -0
- data/Rakefile +2 -0
- data/lib/peekapp.rb +10 -0
- data/lib/peekapp/apps.rb +46 -0
- data/lib/peekapp/base.rb +26 -0
- data/lib/peekapp/config/default.yml +5 -0
- data/lib/peekapp/config/exceptions.yml +4 -0
- data/lib/peekapp/ratings.rb +66 -0
- data/lib/peekapp/reviews.rb +75 -0
- data/lib/peekapp/version.rb +3 -0
- data/peekapp.gemspec +25 -0
- metadata +128 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
peekapp (0.1.0)
|
5
|
+
curb (>= 0.7.9)
|
6
|
+
json (>= 1.4.6)
|
7
|
+
nokogiri (>= 1.4.4)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: http://rubygems.org/
|
11
|
+
specs:
|
12
|
+
curb (0.7.10)
|
13
|
+
json (1.5.0)
|
14
|
+
nokogiri (1.4.4)
|
15
|
+
|
16
|
+
PLATFORMS
|
17
|
+
ruby
|
18
|
+
|
19
|
+
DEPENDENCIES
|
20
|
+
peekapp!
|
data/README.markdown
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
Retrieve reviews and ratings from the App Store.
|
2
|
+
|
3
|
+
# Installation
|
4
|
+
gem install peekapp
|
5
|
+
|
6
|
+
## How to
|
7
|
+
|
8
|
+
For advanced functionnalities you should read the source. Peekapp can only retrieve ratings & reviews from the Canadian App Store (*this will be fixed soon*).
|
9
|
+
|
10
|
+
require "peekapp"
|
11
|
+
|
12
|
+
# Get the App
|
13
|
+
app = Peekapp::Apps::find 390574988 # App ID
|
14
|
+
|
15
|
+
# Get reviews for this App
|
16
|
+
app.reviews
|
17
|
+
# => [#<Review:0x102237a28 @data={
|
18
|
+
# :title => "Amazing",
|
19
|
+
# :comment => "Best Twitter app on the face of the planet!",
|
20
|
+
# :username => "Thomas Gallagher",
|
21
|
+
# :rating => 5,
|
22
|
+
# :user_id => 33308895,
|
23
|
+
# :version => "2.0",
|
24
|
+
# :date => "9-Oct-2009",
|
25
|
+
# :id => 131605495
|
26
|
+
# }>, ...]
|
27
|
+
|
28
|
+
# And the ratings...
|
29
|
+
app.ratings
|
30
|
+
# => [#Rating:0x1029372a23 @data={
|
31
|
+
# :current => { "1": 38, "2": 12, "3": 23, "4": 25, "5": 105 },
|
32
|
+
# :all => { "1": 2736, "2": 749, "3": 1045, "4": 1103, "5": 3880 },
|
33
|
+
# :store_id => "143455-5,12"
|
34
|
+
# }>, ...]
|
35
|
+
|
36
|
+
## Warning
|
37
|
+
Since Peekapp is **scraping** the App Store, you might experience some problems if Apple change iTunes' html layout. I've made some tests and the App Store is updated every 20 minutes (*ballpark*). So don't waste your time trying to get *real time* ratings & reviews.
|
38
|
+
|
39
|
+
## Todo
|
40
|
+
- Create unit tests w/ [FakeWeb](https://github.com/chrisk/fakeweb)
|
41
|
+
- Allow international App Stores
|
42
|
+
- iTunes Connect support
|
data/Rakefile
ADDED
data/lib/peekapp.rb
ADDED
data/lib/peekapp/apps.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module Peekapp
|
2
|
+
|
3
|
+
module Apps
|
4
|
+
|
5
|
+
def self.search query # {{{
|
6
|
+
result = JSON.parse(Peekapp::query(:url => $peekapp_config[:search_url], :keywords => query.gsub(' ', '%20')))
|
7
|
+
result['results'].map { |a| App.new a }
|
8
|
+
end # }}}
|
9
|
+
|
10
|
+
def self.find id # {{{
|
11
|
+
result = JSON.parse(Peekapp::query :url => $peekapp_config[:app_url], :app_id => id)
|
12
|
+
raise AppNotFound if result["resultCount"] < 1
|
13
|
+
result["results"].map{ |a| App.new a }.first
|
14
|
+
end # }}}
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
class App
|
19
|
+
|
20
|
+
attr_accessor :_reviews, :_ratings
|
21
|
+
|
22
|
+
def initialize data # {{{
|
23
|
+
@data = data
|
24
|
+
end # }}}
|
25
|
+
|
26
|
+
def id # {{{
|
27
|
+
@data["trackId"]
|
28
|
+
end # }}}
|
29
|
+
|
30
|
+
def method_missing method # {{{
|
31
|
+
@data[method.to_s]
|
32
|
+
end # }}}
|
33
|
+
|
34
|
+
def ratings options={} # {{{
|
35
|
+
self._ratings = Peekapp::Ratings.from_app self.id, ["143455-5,12"], options if self._ratings.nil? or options[:force_refresh]
|
36
|
+
self._ratings
|
37
|
+
end # }}}
|
38
|
+
|
39
|
+
def reviews options={} # {{{
|
40
|
+
self._reviews = Peekapp::Reviews.from_app self.id, ["143455-5,12"], options if self._reviews.nil? or options[:force_refresh]
|
41
|
+
self._reviews
|
42
|
+
end # }}}
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
data/lib/peekapp/base.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
$peekapp_config = YAML::load(File.open("#{File.dirname(__FILE__)}/config/default.yml"))
|
2
|
+
|
3
|
+
module Peekapp
|
4
|
+
|
5
|
+
def self.query args # {{{
|
6
|
+
c = Curl::Easy.perform(parse_url(args)) do |request|
|
7
|
+
request.headers["User-Agent"] = $peekapp_config[:user_agent]
|
8
|
+
request.headers["X-Apple-Store-Front"] = "#{args[:store_id]}" if args[:store_id]
|
9
|
+
end
|
10
|
+
c.body_str
|
11
|
+
end # }}}
|
12
|
+
|
13
|
+
def self.parse_url data # {{{
|
14
|
+
url = data[:url]
|
15
|
+
data.each_pair{|k,v| url = url.gsub("|#{k}|", v.to_s) if k != :url}
|
16
|
+
url
|
17
|
+
end # }}}
|
18
|
+
|
19
|
+
def self.load_exceptions # {{{
|
20
|
+
exceptions = YAML::load(File.open("#{File.dirname(__FILE__)}/config/exceptions.yml"))
|
21
|
+
exceptions.each { |error| self.module_eval("#{error} = Class.new(StandardError)") }
|
22
|
+
end # }}}
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
Peekapp::load_exceptions
|
@@ -0,0 +1,5 @@
|
|
1
|
+
:user_agent: "iTunes/10.0.1 (Macintosh; Intel Mac OS X 10.6.4) AppleWebKit/533.18.1"
|
2
|
+
:reviews_url: "http://ax.itunes.apple.com/WebObjects/MZStore.woa/wa/customerReviews?update=1&id=|app_id|&appVersion=|app_version|&sort=4&displayable-kind=11&page=|page|"
|
3
|
+
:ratings_url: "http://itunes.apple.com/WebObjects/MZStore.woa/wa/customerReviews?id=|app_id|&displayable-kind=11"
|
4
|
+
:app_url: "http://ax.itunes.apple.com/WebObjects/MZStoreServices.woa/wa/wsLookup?id=|app_id|"
|
5
|
+
:search_url: "http://ax.itunes.apple.com/WebObjects/MZStoreServices.woa/wa/wsSearch?term=|keywords|&entity=software"
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Peekapp
|
2
|
+
|
3
|
+
module Ratings
|
4
|
+
|
5
|
+
def self.from_app id, stores, options = {} # {{{
|
6
|
+
rating = Array.new
|
7
|
+
stores.each do |s|
|
8
|
+
rating << parse({
|
9
|
+
:dom => Peekapp::query({
|
10
|
+
:url => $peekapp_config[:ratings_url],
|
11
|
+
:page => 1,
|
12
|
+
:app_id => id,
|
13
|
+
:store_id => s,
|
14
|
+
:app_version => (options[:app_version] ? options[:app_version] : "all")
|
15
|
+
}),
|
16
|
+
:store_id => s
|
17
|
+
})
|
18
|
+
end
|
19
|
+
rating
|
20
|
+
end # }}}
|
21
|
+
|
22
|
+
def self.parse data # {{{
|
23
|
+
rating = Rating.new :store_id => data[:store_id]
|
24
|
+
dom = Nokogiri::HTML.parse(data[:dom])
|
25
|
+
nb_ratings_section = dom.css("div.ratings-histogram").count
|
26
|
+
|
27
|
+
dom.css("div.ratings-histogram").each_with_index do |r,i|
|
28
|
+
data = Hash.new
|
29
|
+
result = true
|
30
|
+
r.css("div.vote").each_with_index{ |v,j| data.merge!({(5-j) => v.css("span.total").children.to_s.to_i }) }
|
31
|
+
|
32
|
+
if nb_ratings_section === 1 or i === 1
|
33
|
+
rating.set :key => :all, :value => data
|
34
|
+
rating.set :key => :current, :value => data if nb_ratings_section === 1
|
35
|
+
else
|
36
|
+
rating.set :key => :current, :value => data
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
rating.set :key => :all, :value => {1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 0} if nb_ratings_section < 1
|
41
|
+
rating
|
42
|
+
end # }}}
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
class Rating
|
47
|
+
|
48
|
+
def initialize data # {{{
|
49
|
+
@data = data
|
50
|
+
end # }}}
|
51
|
+
|
52
|
+
def id # {{{
|
53
|
+
@data[:id]
|
54
|
+
end # }}}
|
55
|
+
|
56
|
+
def set args # {{{
|
57
|
+
@data.merge!({args[:key] => args[:value]})
|
58
|
+
end # }}}
|
59
|
+
|
60
|
+
def method_missing method # {{{
|
61
|
+
@data[method.to_sym]
|
62
|
+
end # }}}
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Peekapp
|
2
|
+
|
3
|
+
module Reviews
|
4
|
+
|
5
|
+
def self.from_app id, stores, options = {} # {{{
|
6
|
+
reviews = Array.new
|
7
|
+
begin
|
8
|
+
stores.each do |s|
|
9
|
+
args = {
|
10
|
+
:url => $peekapp_config[:reviews_url],
|
11
|
+
:app_id => id,
|
12
|
+
:store_id => s,
|
13
|
+
:page => (options[:page] ? options[:page] : 1),
|
14
|
+
:app_version => (options[:app_version] ? options[:app_version] : "all")
|
15
|
+
}
|
16
|
+
dom = Peekapp::query args
|
17
|
+
begin
|
18
|
+
nb_page = Nokogiri::HTML.parse(dom).css("div.paginated-content").first["total-number-of-pages"].to_i
|
19
|
+
rescue
|
20
|
+
raise ReviewsUnavailableForThisApp
|
21
|
+
end
|
22
|
+
nb_page.times do |z|
|
23
|
+
# dom is already instanciated for z === 0
|
24
|
+
args[:page] = z+1
|
25
|
+
dom = Peekapp::query args if z > 0
|
26
|
+
parse(dom).each do |p|
|
27
|
+
raise LatestReviewReached if p.id === options[:latest_review_id].to_s
|
28
|
+
reviews << p
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
rescue LatestReviewReached
|
33
|
+
# Do nothing... just get out.
|
34
|
+
end
|
35
|
+
reviews
|
36
|
+
end # }}}
|
37
|
+
|
38
|
+
def self.parse data # {{{
|
39
|
+
reviews = Array.new
|
40
|
+
dom = Nokogiri::HTML.parse(data)
|
41
|
+
dom.css("div.customer-review").each do |r|
|
42
|
+
# That's some ugly stuff... I know
|
43
|
+
reviews << Review.new({
|
44
|
+
:id => r.css("a.report").first["href"].split("=").last,
|
45
|
+
:title => r.css("span.customerReviewTitle").children.to_s,
|
46
|
+
:comment => r.css("p.content").children.to_s.gsub("\n", "").gsub(" ", ""),
|
47
|
+
:username => r.css("a.reviewer").children.to_s.gsub("\n", "").gsub(" ", ""),
|
48
|
+
:user_id => r.css("a.reviewer").first["href"].split('=').last,
|
49
|
+
:rating => r.css("div.rating").first['aria-label'].split(' ').first.to_i,
|
50
|
+
:date => r.css("span.user-info").children.to_s.split(" -\n").last.gsub("\n", "").gsub(" ", ""),
|
51
|
+
:version => r.css("span.user-info").children.to_s.split(" -\n")[1].split(" - ").first.split(" ").last.to_s,
|
52
|
+
})
|
53
|
+
end
|
54
|
+
reviews
|
55
|
+
end # }}}
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
class Review
|
60
|
+
|
61
|
+
def initialize data # {{{
|
62
|
+
@data = data
|
63
|
+
end # }}}
|
64
|
+
|
65
|
+
def id # {{{
|
66
|
+
@data[:id]
|
67
|
+
end # }}}
|
68
|
+
|
69
|
+
def method_missing method # {{{
|
70
|
+
@data[method.to_sym]
|
71
|
+
end # }}}
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
data/peekapp.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "peekapp/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "peekapp"
|
7
|
+
s.version = Peekapp::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Samuel Garneau"]
|
10
|
+
s.email = ["samgarneau@gmail.com"]
|
11
|
+
s.homepage = "http://github.com/garno/peekapp"
|
12
|
+
s.summary = %q{Retrieve ratings & reviews from the App Store}
|
13
|
+
s.description = %q{Easily scrape the App Store to retrieve ratings & reviews.}
|
14
|
+
|
15
|
+
#s.rubyforge_project = "peekapp"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
s.add_dependency('nokogiri', '>= 1.4.4')
|
23
|
+
s.add_dependency('json', '>= 1.4.6')
|
24
|
+
s.add_dependency('curb', '>= 0.7.9')
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: peekapp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Samuel Garneau
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-02-17 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: nokogiri
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 15
|
30
|
+
segments:
|
31
|
+
- 1
|
32
|
+
- 4
|
33
|
+
- 4
|
34
|
+
version: 1.4.4
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: json
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 11
|
46
|
+
segments:
|
47
|
+
- 1
|
48
|
+
- 4
|
49
|
+
- 6
|
50
|
+
version: 1.4.6
|
51
|
+
type: :runtime
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: curb
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 17
|
62
|
+
segments:
|
63
|
+
- 0
|
64
|
+
- 7
|
65
|
+
- 9
|
66
|
+
version: 0.7.9
|
67
|
+
type: :runtime
|
68
|
+
version_requirements: *id003
|
69
|
+
description: Easily scrape the App Store to retrieve ratings & reviews.
|
70
|
+
email:
|
71
|
+
- samgarneau@gmail.com
|
72
|
+
executables: []
|
73
|
+
|
74
|
+
extensions: []
|
75
|
+
|
76
|
+
extra_rdoc_files: []
|
77
|
+
|
78
|
+
files:
|
79
|
+
- .gitignore
|
80
|
+
- Gemfile
|
81
|
+
- Gemfile.lock
|
82
|
+
- README.markdown
|
83
|
+
- Rakefile
|
84
|
+
- lib/peekapp.rb
|
85
|
+
- lib/peekapp/apps.rb
|
86
|
+
- lib/peekapp/base.rb
|
87
|
+
- lib/peekapp/config/default.yml
|
88
|
+
- lib/peekapp/config/exceptions.yml
|
89
|
+
- lib/peekapp/ratings.rb
|
90
|
+
- lib/peekapp/reviews.rb
|
91
|
+
- lib/peekapp/version.rb
|
92
|
+
- peekapp.gemspec
|
93
|
+
has_rdoc: true
|
94
|
+
homepage: http://github.com/garno/peekapp
|
95
|
+
licenses: []
|
96
|
+
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options: []
|
99
|
+
|
100
|
+
require_paths:
|
101
|
+
- lib
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
103
|
+
none: false
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
hash: 3
|
108
|
+
segments:
|
109
|
+
- 0
|
110
|
+
version: "0"
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
|
+
none: false
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
hash: 3
|
117
|
+
segments:
|
118
|
+
- 0
|
119
|
+
version: "0"
|
120
|
+
requirements: []
|
121
|
+
|
122
|
+
rubyforge_project:
|
123
|
+
rubygems_version: 1.3.7
|
124
|
+
signing_key:
|
125
|
+
specification_version: 3
|
126
|
+
summary: Retrieve ratings & reviews from the App Store
|
127
|
+
test_files: []
|
128
|
+
|