mobile_stores 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.
@@ -0,0 +1,60 @@
1
+ module MobileStores
2
+ class App
3
+
4
+ attr_accessor :title, :application_id, :creator_name, :creator_id, :version, :rating, :description, :category, :icon_url, :view_url, :price, :compatibility, :screenshot_urls
5
+
6
+ def initialize(hash = {})
7
+ hash.each_pair do |key, value|
8
+ self.send "#{ key }=", value
9
+ end
10
+ end
11
+
12
+ class << self
13
+ attr_accessor :store
14
+
15
+ # Selects the store for further manipulations.
16
+ #
17
+ # App.in(:app_store)
18
+ #
19
+ def in(store)
20
+ store_class = if store.is_a? Symbol
21
+ MobileStores::STORES[store]
22
+ elsif store.is_a? String
23
+ Object.const_get(store) rescue nil
24
+ elsif store.is_a? Class
25
+ store
26
+ end
27
+
28
+ raise NotImplementedError, "#{ store } is not implemented yet." if store.nil?
29
+
30
+ store_class.new
31
+ end
32
+
33
+ # Selects Apple AppStore for further manipulations.
34
+ def app_store
35
+ self.in(AppStore)
36
+ end
37
+
38
+ # Selects Google Play Store for further manipulations.
39
+ def google_play
40
+ self.in(GooglePlay)
41
+ end
42
+
43
+ # Selects Windows Store for further manipulations.
44
+ def windows_store
45
+ self.in(WindowsStore)
46
+ end
47
+
48
+ # Selects BlackBerry World Store for further manipulations.
49
+ def blackberry_world
50
+ self.in(BlackBerryWorld)
51
+ end
52
+
53
+ # Selects BlackBerry World Store for further manipulations.
54
+ def amazon_marketplace
55
+ self.in(AmazonMarketplace)
56
+ end
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,56 @@
1
+ module MobileStores
2
+ class AppStore
3
+ include MobileStore
4
+
5
+ act_as_json_source
6
+
7
+ private
8
+
9
+ def self.apps_url(query = nil, count = nil, country = nil)
10
+ url = "http://itunes.apple.com/search?media=software"
11
+ url = "#{ url }&term=#{ CGI::escape(query) }" if query
12
+ url = "#{ url }&limit=#{ count }" if count
13
+ url = "#{ url }&country=#{ country.alpha2 }"
14
+ url
15
+ end
16
+
17
+ def self.app_url(id, country)
18
+ "http://itunes.apple.com/lookup?id=#{ CGI::escape(id) }&country=#{ country.alpha2 }"
19
+ end
20
+
21
+ def self.process_app_json(json)
22
+ app = self.parse_json(json).first
23
+ raise "App not found." if app.nil?
24
+ app
25
+ end
26
+
27
+ def self.process_apps_json(json)
28
+ self.parse_json(json)
29
+ end
30
+
31
+ def self.parse_json(json)
32
+ if json['results']
33
+ json['results'].map do |app|
34
+ App.new({
35
+ application_id: app['trackId'].to_s.force_encoding('utf-8'),
36
+ title: app['trackName'].force_encoding('utf-8'),
37
+ creator_name: app['artistName'].force_encoding('utf-8'),
38
+ creator_id: app['artistId'].to_s.force_encoding('utf-8'),
39
+ version: app['version'].to_s.force_encoding('utf-8'),
40
+ rating: (app['averageUserRating'].to_f || 0).to_s.force_encoding('utf-8').to_f,
41
+ description: app['description'].force_encoding('utf-8'),
42
+ category: app['primaryGenreName'].force_encoding('utf-8'),
43
+ icon_url: app['artworkUrl512'].force_encoding('utf-8'),
44
+ view_url: app['trackViewUrl'].force_encoding('utf-8'),
45
+ price: app['price'].to_s.force_encoding('utf-8').to_d,
46
+ compatibility: app['supportedDevices'].sort,
47
+ screenshot_urls: app['screenshotUrls'] + app['ipadScreenshotUrls']
48
+ })
49
+ end
50
+ else
51
+ []
52
+ end
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,55 @@
1
+ module MobileStores
2
+ class BlackBerryWorld
3
+ include MobileStore
4
+
5
+ act_as_json_source
6
+
7
+ private
8
+
9
+ def self.apps_url(query = nil, count = nil, country)
10
+ url = "http://appworld.blackberry.com/cas/producttype/apps/listtype/search_listing/category/0/search/#{ CGI::escape(query) }?page=1&model=Imagination%20GPU&countrycode=#{ country.alpha2 }&lang=#{ country.languages.first }"
11
+ url = "#{ url }&pagesize=#{ count }" if count
12
+ url
13
+ end
14
+
15
+ def self.app_url(id, country)
16
+ "http://appworld.blackberry.com/cas/content/#{ id }?model=Imagination%20GPU&countrycode=#{ country.alpha2 }&lang=#{ country.languages.first }"
17
+ end
18
+
19
+ def self.process_app_json(json)
20
+ self.parse_element_json(json)
21
+ end
22
+
23
+ def self.process_apps_json(json)
24
+ self.parse_list_json(json)
25
+ end
26
+
27
+ def self.parse_element_json(json)
28
+ App.new({
29
+ application_id: json['id'],
30
+ title: json['name'],
31
+ creator_name: json['vendorName'],
32
+ creator_id: json['vendorId'],
33
+ version: (json['cdDTO']['releaseVersion'] rescue nil),
34
+ rating: (json['rating'] == -1 ? nil : json['rating']),
35
+ description: json['description'],
36
+ category: json['categories'][0]['name'],
37
+ icon_url: "http://appworld.blackberry.com/webstore/servedimages/#{ json['iconId'] }.png/?t=2",
38
+ view_url: "http://appworld.blackberry.com/webstore/content/#{ json['id'] }",
39
+ price: (json['cdDTO']['price'].to_d rescue 0),
40
+ compatibility: json['cdDTO']['supportedDevices'],
41
+ screenshot_urls: (json['screenShots'] || []).map{ |s| "http://appworld.blackberry.com/webstore/servedimages/#{ s }.png/?t=11" }
42
+ })
43
+ end
44
+
45
+ def self.parse_list_json(json)
46
+ json['ldata'].map do |app|
47
+ App.new({
48
+ :application_id => app['id'],
49
+ :title => app['name'],
50
+ :icon_url => "http://appworld.blackberry.com/webstore/servedimages/#{ app['iconId'] }.png/?t=2"
51
+ })
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,61 @@
1
+ # GooglePlay doesn't support country based filtering, so every country is
2
+ # supported by default
3
+ module MobileStores
4
+ class GooglePlay
5
+ include MobileStore
6
+
7
+ act_as_html_source
8
+
9
+ private
10
+
11
+ def self.apps_url(query = nil, count = nil, country = nil)
12
+ url = "https://play.google.com/store/search?hl=en"
13
+ url = "#{ url }&q=#{ CGI::escape(query) }" if query
14
+ url = "#{ url }&start=0&num=#{ count }" if count
15
+ url
16
+ end
17
+
18
+ def self.app_url(id, country)
19
+ "https://play.google.com/store/apps/details?id=#{ id }&hl=en"
20
+ end
21
+
22
+ def self.process_app_html(doc)
23
+ self.parse_html_page(doc)
24
+ end
25
+
26
+ def self.process_apps_html(doc, count = nil)
27
+ self.parse_html_list(doc, count)
28
+ end
29
+
30
+ def self.parse_html_page(doc)
31
+ App.new({
32
+ application_id: doc.css('.details-wrapper')[0]['data-docid'],
33
+ title: doc.css('.document-title div')[0].inner_text.strip,
34
+ creator_name: doc.css('.document-subtitle.primary')[0].inner_text.strip,
35
+ creator_id: doc.css('.document-subtitle.primary')[0].inner_text.strip,
36
+ version: doc.css('[itemprop=softwareVersion]')[0].inner_text.strip,
37
+ rating: doc.css('[itemprop=ratingValue]')[0]['content'].to_f,
38
+ description: doc.css('[itemprop=description] .app-orig-desc')[0].inner_text.strip.gsub(%r{</?[^>]+?>}, ''),
39
+ category: doc.css('.document-subtitle.category')[0].inner_text.strip,
40
+ icon_url: doc.css('.cover-image')[0]['src'],
41
+ view_url: "https://play.google.com/store/apps/details?hl=en&id=#{ doc.css('.details-wrapper')[0]['data-docid'] }",
42
+ price: doc.css('[itemprop=price]')[0]["content"].to_d,
43
+ compatibility: (doc.css('.metadata div.content[itemprop=operatingSystems]')[0].inner_text.strip rescue nil),
44
+ screenshot_urls: doc.css('.full-screenshot').map{ |d| d['src'] }
45
+ })
46
+ end
47
+
48
+ def self.parse_html_list(doc, count = nil)
49
+ apps = doc.css('.card-list .card').map do |app|
50
+ App.new({
51
+ :application_id => app['data-docid'],
52
+ :title => app.css('img')[0]['alt'],
53
+ :icon_url => app.css('img')[0]['src']
54
+ })
55
+ end
56
+
57
+ count ? apps.take(count) : apps
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,176 @@
1
+ require "json"
2
+ require "net/http"
3
+ require "uri"
4
+
5
+ module MobileStores
6
+ module MobileStore
7
+
8
+ module InstanceMethods
9
+
10
+ attr_accessor :country
11
+
12
+ def initialize
13
+ # set country to United States as default
14
+ @country = Country.new(:us)
15
+ super
16
+ end
17
+
18
+ # Selects store corresponding to country or returns selected country,
19
+ # if arguments is not specified
20
+ #
21
+ # App.in(:app_store).country(:us).find("12345") # returns +App+ object
22
+ #
23
+ def country(country = nil)
24
+ if country
25
+ @country = Country.new(country)
26
+ self
27
+ else
28
+ @country
29
+ end
30
+ end
31
+
32
+ # Finds app in store by ID.
33
+ # Returns nil if app was not found.
34
+ #
35
+ # App.in(:app_store).find("12345") # returns +App+ object
36
+ #
37
+ def find(id)
38
+ find!(id) rescue nil
39
+ end
40
+
41
+ # Finds app in store by ID.
42
+ # Raises error if app was not found.
43
+ #
44
+ # App.in(:app_store).find("12345") # returns +App+ object
45
+ #
46
+ def find!(id)
47
+ self.class.find_app(id, country)
48
+ end
49
+
50
+ # Searches apps in store by query
51
+ #
52
+ # App.in(:app_store).search("angry") # returns array of +App+ objects
53
+ #
54
+ def search(query, count = nil)
55
+ self.class.search_apps(query, count, country)
56
+ end
57
+
58
+ # Returns true if app exists in store searching by ID,
59
+ # otherwise returns false
60
+ def exists?(id)
61
+ not find(id).nil?
62
+ end
63
+
64
+ end
65
+
66
+ module ClassMethods
67
+
68
+ def act_as_json_source
69
+ self.extend(JsonClassMethods)
70
+ end
71
+
72
+ def act_as_html_source
73
+ self.extend(HtmlClassMethods)
74
+ end
75
+
76
+ # Returns url for a list of apps.
77
+ def apps_url(query = nil, count = nil)
78
+ raise_not_implemented
79
+ end
80
+
81
+ # Returns url for a single app.
82
+ def app_url(id)
83
+ raise_not_implemented
84
+ end
85
+
86
+ private
87
+
88
+ # Raises +NotImplementedError+ when called.
89
+ def raise_not_implemented
90
+ raise NotImplementedError, "#{ self.name.to_s }.#{ caller[0] =~ /`([^']*)'/ and $1 } is not implemented."
91
+ end
92
+ end
93
+
94
+ module JsonClassMethods
95
+
96
+ # Finds app in store by ID.
97
+ #
98
+ # AppStore.find_app("12345") # returns +App+ object
99
+ #
100
+ def find_app(id, country)
101
+ process_app_json(json(app_url(id, country)))
102
+ end
103
+
104
+ # Searches apps in store by query.
105
+ #
106
+ # AppStore.search_apps("angry") # returns array of +App+ objects
107
+ #
108
+ def search_apps(query, count = nil, country)
109
+ process_apps_json(json(apps_url(query, count, country)))
110
+ end
111
+
112
+ private
113
+
114
+ # Processes json for a list of apps.
115
+ def process_apps_json(json)
116
+ raise_not_implemented
117
+ end
118
+
119
+ # Process json for a single app.
120
+ def process_app_json(json)
121
+ raise_not_implemented
122
+ end
123
+
124
+ # Fetches json object from url.
125
+ def json(url)
126
+ response = Net::HTTP.get_response(URI.parse(url))
127
+ JSON.parse(response.body)
128
+ end
129
+
130
+ end
131
+
132
+ module HtmlClassMethods
133
+
134
+ # Finds app in store by ID.
135
+ #
136
+ # AppStore.find_app("12345") # returns +App+ object
137
+ #
138
+ def find_app(id, country)
139
+ process_app_html(doc(app_url(id, country)))
140
+ end
141
+
142
+ # Searches apps in store by query.
143
+ #
144
+ # AppStore.search_apps("angry") # returns array of +App+ objects
145
+ #
146
+ def search_apps(query, count = nil, country)
147
+ process_apps_html(doc(apps_url(query, count, country)), count)
148
+ end
149
+
150
+ private
151
+
152
+ # Processes json for a list of apps.
153
+ def process_apps_html(html, count)
154
+ raise_not_implemented
155
+ end
156
+
157
+ # Process json for a single app.
158
+ def process_app_html(html)
159
+ raise_not_implemented
160
+ end
161
+
162
+ # Fetches json object from url.
163
+ def doc(url)
164
+ Nokogiri::HTML(open(url))
165
+ end
166
+
167
+ end
168
+
169
+ def self.included(base)
170
+ base.extend(ClassMethods)
171
+ end
172
+
173
+ include InstanceMethods
174
+
175
+ end
176
+ end
@@ -0,0 +1,72 @@
1
+ module MobileStores
2
+ class WindowsStore
3
+ include MobileStore
4
+
5
+ COUNTRY_LANGUAGE = { :ao => 'pt', :ar => 'es', :am => 'en', :au => 'en', :la => 'az', :az => 'tn', :bd => 'en', :be => 'nl', :be => 'fr', :bj => 'fr', :bo => 'es', :br => 'pt', :bf => 'fr', :bi => 'fr', :cm => 'en', :ca => 'en', :ca => 'fr', :cz => 'cs', :cl => 'es', :co => 'es', :cd => 'fr', :cr => 'es', :ci => 'fr', :dk => 'da', :de => 'de', :ec => 'es', :ee => 'et', :sv => 'es', :es => 'es', :es => 'ca', :fr => 'fr', :gh => 'en', :gt => 'es', :gn => 'fr', :ht => 'fr', :hn => 'es', :hk => 'en', :hr => 'hr', :is => 'is', :in => 'en', :id => 'id', :ie => 'en', :it => 'it', :ke => 'en', :kw => 'en', :lv => 'lv', :li => 'de', :lt => 'lt', :mg => 'fr', :hu => 'hu', :mw => 'en', :my => 'ms', :my => 'en', :ml => 'fr', :mx => 'es', :mz => 'pt', :nl => 'nl', :nz => 'en', :ni => 'es', :ne => 'fr', :ng => 'en', :no => 'nb', :at => 'de', :la => 'uz', :uz => 'tn', :pk => 'en', :py => 'es', :pe => 'es', :ph => 'en', :ph => 'il', :pl => 'pl', :pt => 'pt', :do => 'es', :ro => 'ro', :rw => 'fr', :ch => 'de', :sn => 'fr', :al => 'sq', :sl => 'en', :sg => 'en', :si => 'sl', :sk => 'sk', :so => 'en', :za => 'en', :la => 'sr', :cs => 'tn', :ch => 'fr', :fi => 'fi', :se => 'sv', :ch => 'it', :tz => 'en', :td => 'fr', :tg => 'fr', :tr => 'tr', :ug => 'en', :gb => 'en', :us => 'en', :ve => 'es', :vn => 'vi', :zm => 'en', :zw => 'en', :gr => 'el', :by => 'be', :bg => 'bg', :kz => 'kk', :mk => 'mk', :ru => 'ru', :tj => 'ru', :tm => 'ru', :ua => 'uk', :il => 'he', :jo => 'ar', :ae => 'ar', :bh => 'ar', :dz => 'ar', :iq => 'ar', :kw => 'ar', :ma => 'ar', :sa => 'ar', :ye => 'ar', :tn => 'ar', :om => 'ar', :qa => 'ar', :eg => 'ar', :in => 'hi', :th => 'th', :kr => 'ko', :cn => 'zh', :tw => 'zh', :jp => 'ja', :hk => 'zh'
6
+ }
7
+
8
+ act_as_html_source
9
+
10
+ private
11
+
12
+ def self.apps_url(query = nil, count = nil, country)
13
+ url = "http://www.windowsphone.com/#{ COUNTRY_LANGUAGE[country.alpha2.downcase.to_sym] }-#{ country.alpha2.downcase }/store/search"
14
+ url = "#{ url }?q=#{ CGI::escape(query) }" if query
15
+ URI.encode url
16
+ end
17
+
18
+ def self.app_url(id, country)
19
+ URI.encode "http://www.windowsphone.com/#{ country.languages.first }-#{ country.alpha2.downcase }/store/app/#{ id }"
20
+ end
21
+
22
+ def self.process_app_html(doc)
23
+ app = self.parse_html_page(doc)
24
+ raise "App not found." if app.nil?
25
+ app
26
+ end
27
+
28
+ def self.process_apps_html(doc, count = nil)
29
+ self.parse_html_list(doc, count)
30
+ end
31
+
32
+ def self.parse_html_page(doc)
33
+ App.new({
34
+ application_id: doc.css('[itemprop=url]')[0]['content'].match(/[^\/]+\/[^\/]+$/)[0],
35
+ title: doc.css('[itemprop=name]')[0].inner_text,
36
+ creator_name: doc.css('[itemprop=publisher]')[0].inner_text,
37
+ creator_id: doc.css('[itemprop=publisher]')[0].inner_text,
38
+ version: doc.css('[itemprop=softwareVersion]')[0].inner_text,
39
+ rating: doc.css('[itemprop=ratingValue]')[0]['content'].to_f,
40
+ description: doc.css('[itemprop=description]')[0].inner_text.gsub(%r{</?[^>]+?>}, ''),
41
+ category: (doc.css('[itemprop=applicationCategory]')[0].inner_text.capitalize rescue nil),
42
+ icon_url: doc.css('[itemprop=image]')[0]['src'],
43
+ view_url: doc.css('[itemprop=url]')[0]['content'],
44
+ price: doc.css('[itemprop=price]')[0].inner_text.gsub(/[^0-9.,]/, '').to_d,
45
+ compatibility: doc.css('[itemprop=operatingSystems]').map{ |o| o.inner_text}.join(', '),
46
+ screenshot_urls: doc.css('#screenshots a').map{ |d| d['href'] }
47
+ })
48
+ end
49
+
50
+ def self.parse_html_list(doc, count = nil, page = 1)
51
+ apps = doc.css('.appList td').map do |app|
52
+ id_string = app.css('a')[0]['data-ov'].split(';').last
53
+ App.new({
54
+ :application_id => id_string.split(' ')[1] + '/' + id_string.split(' ')[0],
55
+ :title => app.css('a')[1].inner_text,
56
+ :icon_url => app.css('img')[0]['src']
57
+ })
58
+ end
59
+
60
+ if count and page == 1
61
+ while apps.size < count and doc.css('#nextLink').length > 0
62
+ page += 1
63
+ doc = Nokogiri::HTML(open("http://www.windowsphone.com#{ doc.css('#nextLink')[0]['href'] }"))
64
+ apps += self.parse_html_list(doc)
65
+ end
66
+ end
67
+
68
+ count ? apps.take(count) : apps
69
+ end
70
+
71
+ end
72
+ end