peekapp 0.1.0
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.
- 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
|
+
|