Skip to content

Instantly share code, notes, and snippets.

@doomspork
Created March 5, 2013 18:40
Show Gist options
  • Save doomspork/5092938 to your computer and use it in GitHub Desktop.
Save doomspork/5092938 to your computer and use it in GitHub Desktop.
Get apps and their attributes from Google Play.
require 'net/https'
require 'nokogiri'
require 'uri'
module Play
STORE_IDENTIFIER_PATTERN = /(?:[\w\d]+\.)+[\w\d]+/i
class InvalidStoreId < StandardError; end
class Reader
def self.document_from_url(url)
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
request = Net::HTTP::Get.new(uri.request_uri)
response = http.request(request)
Nokogiri::HTML(response.body) if response.kind_of? Net::HTTPOK
end
end
class App
URL_TEMPLATE = "https://play.google.com/store/apps/details?id=<identifier>"
def initialize(options={})
raise StandardError if options[:document].nil?
@content = options[:document].at_css('div.page-content')
#Local cache of DOM nodes to allow for lazy loading
@cached = {}
end
def title
@title ||= title_container.at_css('h1.doc-banner-title').content.to_s
end
def developer
@developer ||= title_container.at_css('a.doc-header-link').content.to_s
end
def description
#TODO
end
def updated
@updated ||= metadata_container.at_css('dd > time[itemprop="datePublished"]').content.to_s
end
def rating
@rating ||= rating_container.at_css('div.ratings').attribute('content').content.to_f
end
def votes
@votes ||= rating_container.at_css('span[itemprop="ratingCount"]').attribute('content').content.to_i
end
def size
@size ||= metadata_container.at_css('dd[itemprop="fileSize"]').content.to_s
end
def price
@prize ||= metadata_container.at_css('dd[itemprop="offers"] > span[itemprop="price"]').attribute('content').content.to_f
end
def content_rating
#TODO
end
def self.search(identifier)
raise InvalidStoreId unless identifier =~ Play::STORE_IDENTIFIER_PATTERN
url = self.app_url(identifier)
document = Play::Reader.document_from_url(url)
App.new({:document => document})
end
def self.app_url(identifier)
URL_TEMPLATE.gsub(/<identifier>/, identifier)
end
private
def title_container
@cached[:title_container] ||= @content.at_css('table.layout-table > tr > td > div > div.doc-banner-container > table.doc-banner-table > tr > td.doc-banner-title-container')
end
def details_container
@cached[:details_container] ||= @content.at_css('table.layout-table > tr > td.content-cell > div.doc-overview-tab')
end
def metadata_container
@cached[:metadata_container] ||= details_container.at_css('div.doc-metadata > dl.doc-metadata-list')
end
def rating_container
@cached[:ratings_container] ||= metadata_container.at_css('dd[itemprop="aggregateRating"]')
end
end
end
describe Play::App do
describe ".search" do
context "with invalid store identifier" do
it "should throw exception" do
expect {Play::App.search("com") }.to raise_error(Play::InvalidStoreId)
end
end
context "with valid store identifier" do
subject do
identifier = "com.tapjoy.tapdefense"
app = Play::App.search(identifier)
end
it { should be_an_instance_of(Play::App) }
its(:title) { should eq "TapDefense" }
its(:size) { should eq "6.1M" }
its(:developer) { should eq "Tapjoy" }
its(:updated) { should eq "December 5, 2012" }
its(:rating) { should eq 3.5 }
its(:votes) { should eq 3455 }
its(:price) { should eq 0 }
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment